Java面試必問之Hashmap底層實現原理(JDK1.7)

1. 前言

Hashmap可以說是Java面試必問的,一般的面試題會問:

  • Hashmap有哪些特性?
  • Hashmap底層實現原理(getputresize)
  • Hashmap怎麼解決hash衝突?
  • Hashmap是執行緒安全的嗎?

今天就從源碼角度一探究竟。筆者的源碼是OpenJDK1.7

2. 構造方法

首先看構造方法的源碼

    // 默認初始容量      static final int DEFAULT_INITIAL_CAPACITY = 16;      // 默認負載因子      static final float DEFAULT_LOAD_FACTOR = 0.75f;      // 數組, 該數據不參與序列化      transient Entry[] table;        public HashMap() {          this.loadFactor = DEFAULT_LOAD_FACTOR;          // 初始容量16,擴容因子0.75,擴容臨界值12          threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);          // 基礎結構為Entry數組          table = new Entry[DEFAULT_INITIAL_CAPACITY];          init();      }

由以上源碼可知,Hashmap的初始容量默認是16, 底層存儲結構是數組(到這裡只能看出是數組, 其實還有鏈表,下邊看源碼解釋)。基本存儲單元是Entry,那Entry是什麼呢?我們接著看Entry相關源碼,

    static class Entry<K,V> implements Map.Entry<K,V> {          final K key;          V value;          Entry<K,V> next;    // 鏈表後置節點          final int hash;            /**           * Creates new entry.           */          Entry(int h, K k, V v, Entry<K,V> n) {              value = v;              next = n;   // 頭插法: newEntry.next=e              key = k;              hash = h;          }          ...      }

由Entry源碼可知,Entry是鏈表結構。綜上所述,可以得出:
Hashmap底層是基於數組和鏈表實現的

3. Hashmap中put()過程

我已經將put過程繪製了流程圖幫助大家理解

先上put源碼

    public V put(K key, V value) {          if (key == null)              return putForNullKey(value);          // 根據key計算hash          int hash = hash(key.hashCode());          // 計算元素在數組中的位置          int i = indexFor(hash, table.length);          // 遍歷鏈表,如果相同覆蓋          for (Entry<K,V> e = table[i]; e != null; e = e.next) {              Object k;              if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {                  V oldValue = e.value;                  e.value = value;                  e.recordAccess(this);                  return oldValue;              }          }            modCount++;          // 頭插法插入元素          addEntry(hash, key, value, i);          return null;      }

上圖中多次提到頭插法,啥是 頭插法 呢?接下來看 addEntry 方法

    void addEntry(int hash, K key, V value, int bucketIndex) {          // 取出原bucket鏈表          Entry<K,V> e = table[bucketIndex];          // 頭插法          table[bucketIndex] = new Entry<>(hash, key, value, e);          // 判斷是否需要擴容          if (size++ >= threshold)              // 擴容好容量為原來的2倍              resize(2 * table.length);      }

結合Entry類的構造方法,每次插入新元素的時候,將bucket原鏈表取出,新元素的next指向原鏈表,這就是 頭插法 。為了更加清晰的表示Hashmap存儲結構,再繪製一張存儲結構圖。

4. Hashmap中get()過程

get()邏輯相對比較簡單,如圖所示

我們來對應下get()源碼

    public V get(Object key) {          // 獲取key為null的值          if (key == null)              return getForNullKey();          // 根據key獲取hash          int hash = hash(key.hashCode());          // 遍歷鏈表,直到找到元素          for (Entry<K,V> e = table[indexFor(hash, table.length)];               e != null;               e = e.next) {              Object k;              if (e.hash == hash && ((k = e.key) == key || key.equals(k)))                  return e.value;          }          return null;      }

5. Hashmap中resize()過程

只要是新插入元素,即執行addEntry()方法,在插入完成後,都會判斷是否需要擴容。從addEntry()方法可知,擴容後的容量為原來的2倍。

    void resize(int newCapacity) {          Entry[] oldTable = table;          int oldCapacity = oldTable.length;          if (oldCapacity == MAXIMUM_CAPACITY) {              threshold = Integer.MAX_VALUE;              return;          }          // 新建數組          Entry[] newTable = new Entry[newCapacity];          // 數據遷移          transfer(newTable);          // table指向新的數組          table = newTable;          // 新的擴容臨界值          threshold = (int)(newCapacity * loadFactor);      }

這裡有個transfer()方法沒講,別著急,擴容時執行緒安全的問題出現在這個方法中,接下來講解數組複製過程。

6. Hashmap擴容安全問題

大家都知道結果: 多執行緒擴容有可能會形成環形鏈表,這裡用圖給大家模擬下擴容過程。

首先看下單執行緒擴容的頭插法

然後看下多執行緒可能會出現的問題

以下是源碼,你仔細品一品

    void transfer(Entry[] newTable) {          Entry[] src = table;          int newCapacity = newTable.length;          for (int j = 0; j < src.length; j++) {              Entry<K,V> e = src[j];              if (e != null) {                  // 釋放舊Entry數組的對象引用                  src[j] = null;                  do {                      Entry<K,V> next = e.next;                      // 重新根據新的數組長度計算位置(同一個bucket上元素hash相等,所以擴容後必然還在一個鏈表上)                      int i = indexFor(e.hash, newCapacity);                      // 頭插法(同一位置上新元素總會被放在鏈表的頭部位置),將newTable[i]的引用賦給了e.next                      e.next = newTable[i];                      // 將元素放在數組上                      newTable[i] = e;                      // 訪問下一個元素                      e = next;                  } while (e != null);              }          }      }

7. Hashmap尋找bucket位置

    static int indexFor(int h, int length) {          // 根據hash與數組長度mod運算          return h & (length-1);      }

由源碼可知, jdk根據key的hash值和數組長度做mod運算,這裡用位運算代替mod。

hash運算值是一個int整形值,在java中int佔4個位元組,32位,下邊通過圖示來說明位運算。

8. AD

如果您覺得還行,請關注公眾號【當我遇上你】, 您的支援是我輸出的最大動力。
同時,歡迎大家一起交流學習。