了解了HashMap底層實現原理后,很容易的能推導出HashMap元素插入的步驟,先計算元素hash值,然后mod哈希表長度得到應存入的桶的下標,最后掛鏈,看一下源碼。
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i;
//哈希表為空或長度為0,對其進行初始化 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length;
//元素所在的桶不包含鍵值對,直接將鍵值對的引用存入桶中 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else {
//節點e存儲的是 插入節點的引用 Node<K,V> e; K k;
//如果key的值以及哈希值和桶中第一個鍵值對相等,將e指向該鍵值對 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p;
//如果桶中節點類型為樹形節點,則使用紅黑樹的插入方式 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else {
//遍歷鏈表 for (int binCount = 0; ; ++binCount) {
//鏈表遍歷到最后仍沒有發現鏈中包含 與要插入的鍵值對相同鍵值的節點 則將要插入的節點掛在該鏈表的最后 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null);
//如果新節點掛鏈后桶中鏈表的長度大於樹化閾值 對鏈表進行樹化操作 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; }
//當前鏈表包含要插入的鍵值時,終止遍歷 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } }
//e!=null的情況表示桶中鏈表存在要插入的節點的鍵值 if (e != null) { // existing mapping for key V oldValue = e.value;
//onlyIfAbsent表示是否僅在oldValue為空的情況下更新鍵值對的值 if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount;
//鍵值對數量超過閾值,擴容操作 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
HashMap插入元素主要步驟解析我已用注釋說明,應該不難看懂,這里還想說一下的是onlyIfAbsent這個變量,這個變量表示的是:在桶中鏈表存在要插入的節點的鍵值時,是否僅在舊值為空的情況下更新鍵值對的值,可以看到put方法這個參數傳值為false,所以在HashMap在插入節點時,若key值相同,新值替換舊值。
PS. 在HashMap中,在使用自定義類作為鍵的類型時,如果自定義類重寫equals方法,則一定要重寫hashCode方法,在源碼中可以看到,HashMap在比較key是否相等時,首先比較key的哈希值,然后再使用equals,如果重寫equals方法未重寫hashCode方法,則會出現兩個對象實際上是“相等”的,但在HashMap中並不認為是兩個相同的key,導致在節點插入時key值相同的節點的值不會被更新,而是被視作一個新節點,或者查找操作時無法根據key值找到相應的value。總的來說,key值的比較主要遵循以下兩點:
1、如果兩個對象相同(即用equals比較返回true),那么它們的hashCode值一定要相同。
2、如果兩個對象的hashCode相同,它們並不一定相同(即用equals比較返回false)。
接下來就要說一下Hash Map的擴容機制了。HashMap的擴容主要分為兩步,第一步將桶的長度擴至兩倍,第二步計算哈希值,重新掛鏈。
源碼如下,有點長。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//table不為空,表明已經初始化過了 if (oldCap > 0) {
//容量已達到最大值,不再擴容,閾值調至最大 if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//按舊容量和閾值的二倍計算新容量和閾值 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//table未被初始化,將threshold 的值賦值給 newCap
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//table未被初始化,設置容量為默認容量,閾值為容量和負載因子的乘積 else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//重新按照公式計算閾值 if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//創建新的桶數組,完成初始化
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//舊的哈希表不為空,重新掛鏈 if (oldTab != null) {
//遍歷桶 for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//桶中存在鍵值對 if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//桶中只存在一個鍵值對,計算哈希值放入新的哈希表的桶中 if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//如果是樹形節點,對紅黑樹進行拆分 else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//遍歷鏈表,按原順序分組 else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//映射至新桶 if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
我們分步解析一下:
1 )計算新的容量和新的閾值
整個計算過程對應以上源碼的第一個和第二個條件分支,大致如下:
//第一個條件分支
if (oldCap > 0) {
//嵌套分支 if (oldCap >= MAXIMUM_CAPACITY) {...} else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY){...}
} else if (oldThr > 0) {...} else {...}
//第二個條件分支 if (newThr == 0) {...}
第一個條件分支覆蓋情況如下:
- oldCap > 0 :table已被初始化。
- oldCap == 0 && oldThr > 0 :table未被初始化且閾值>0,調用HashMap(int)和HashMap(int,float)構造方法會產生這種情況,這種情況下newCap = threshold = tableSizeFor(initialCapacity),新閾值在第二個條件分支計算。
- oldCap == 0 && oldThr == 0 :table未被初始化且閾值=0,調用HashMap()構造方法產生這種情況,這首設置容量為默認值,通過公式計算閾值。
第二個條件分支覆蓋情況如下:
- newThr == 0 :第一個條件分支未計算新閾值或在計算過程中新閾值溢出歸零,根據公式計算閾值。
以上為新容量和新閾值的計算過程。
2 )節點計算哈希值,重新掛鏈
在Java8中,重新映射節點時需要判斷節點類型,如果是樹形節點,需要對其先進行拆分再映射,如果是鏈表類型節點,則要對其先分組再映射,本文只對鏈表鏈表類型的節點映射進行分析。
桶的下標的計算方式為(n - 1) & hash,假設舊的哈希表的容量為16,兩個元素的哈希值不同,但與n-1進行與運算后,由於只有后四位參與運算,得到的桶的下標相同。
哈希表擴容后容量變為32,得到的桶的下標發生了變化,如下圖所示。
擴容后,參與運算的位數變成了五位,由於兩個哈希值的第五位不同,所以得到的桶的下標也就不相同了,所以說對鏈表類型的節點分組就是將新位置的節點同原位置的節點分開,將桶中的一條鏈表拆分成兩條,並且保證原有的順序。HashMap通過(e.hash & oldCap) == 0判斷節點是否為新位置節點,如下圖所示。
擴容方法中使用loHead、loTail、hiHead、hiTail四個節點存放兩條鏈表的頭節點與末尾節點的指針。
loHead:原位置節點鏈表頭節點指針
loTail:原位置節點鏈表末尾節點指針
hiHead:新位置節點鏈表頭節點指針
hiTail:新位置節點鏈表末尾節點指針
最后將兩條鏈表存放至相應的桶中。
以上就是HashMap插入鍵值對以及擴容部分的源碼解析。
參考資料:https://www.cnblogs.com/chn58/p/6544599.html
https://segmentfault.com/a/1190000012926722