擴容原理
- 首先明確一下擴容以後所有node節點本質還是通過(n-1)&hash 得到索引,然後放入對應的node數組槽位中,但是jdk的開發者在這裏使用了等價的計算方式加速了rehash的過程,將所有的node節點分成了需要移動和不需要移動兩個鏈表,然後一次性移動到對應的位置上
- 要明白這個等價的計算方式是什麼,需要一個直觀的對比就可以很容易的理解,假設map當前容量是n=16(n-1 對應的二進制是0000 1111) ,node的hash值的二進制是1010 0101,那麼在擴容前node 的索引的計算是通過如下方式得到
擴容前計算索引 | |
---|---|
1010 0101 | |
& | 0000 1111 |
0000 0101 | |
索引結果 | 5 |
擴容以後容量是n=32(對應的二進制是0001 1111),node本身的hash值是不變的,仍然是1010 0101,那麼擴容後node 的索引的計算是通過如下方式得到
擴容後計算索引 | |
---|---|
1010 0101 | |
& | 0001 1111 |
0000 0101 | |
索引結果 | 5 |
發現計算後的索引結果和擴容前是一樣的,那麼是什麼原因導致擴容前後的索引是一樣的?或者是不一樣的呢?我們把擴容前後的兩次hash計算索引的過程放在一起對比一下
擴容前計算索引 | 擴容後計算索引 | ||
---|---|---|---|
(hash) | 1010 0101 | 101 0 0101 | |
(n-1)& | 0000 1111 | & | 000 1 1111 |
0000 0101 | 0000 0101 | ||
索引結果 | 5 | 5 |
先給出結論,注意看一下(n-1) 的值他轉化爲二進制以後的第五位,也就是表中加粗的數字
- 它的左邊高位都是0,做&運算以後無論hash值中對應的位是0或1,最後結果都是0,也就是不影響最後的計算結果
- 它的右邊高位都是1,做&運算以後無論hash值中對應的位是0或1,最後結果都和原來的值保持一致,這是&的計算特性
- 結合上兩條特點,那麼無論擴容前後,低四位&以後的計算結果都是一樣的都是5(0101),唯一影響計算結果的就是第5位,也就是說node的hash值的二進制如果第5位是0,那麼擴容後索引不變,如果是1,擴容後的索引就變了,那麼會變成什麼呢?舉個具體例子看一下
- 假設現在node的hash值是1011 0101,相比之前的node,現在例子中的hash值二進制第五位是1
擴容前計算索引 | 擴容後計算索引 | ||
---|---|---|---|
(hash) | 1011 0101 | 101 1 0101 | |
&(n-1) | 0000 1111 | & | 000 1 1111 |
0001 0101 | 000 1 0101 | ||
索引結果 | 5 | 16 + 5 |
我們發現,最後擴容以後計算結果中第5位變成了1,低四位不變,也就是16 + 5,而這增加的16恰好是map擴容前的容量16,回到上面的問題,node的hash值的二進制如果第5位是1,擴容後的索引就是 擴容前的容量 + 原索引值。
- 那麼爲什麼是第五位呢?因爲擴容一倍以後,n-1的二進制位從4個1(1111)變爲了5個1(11111),影響計算結果的恰恰是多出來的那一位1,那麼現在可以再總結一下上面的規律,如何判斷一個node在擴容後索引是否變化?如果多出來的那一位1和hash做位與運算結果中第五位是0,那麼擴容後索引不變,如果是1那麼擴容後的索引就是 擴容前的容量 + 原索引值
- 根據上面的結論,計算結果就可以簡化爲如下過程
hash值二進制第五位是1 | hash值二進制第五位是0 | ||
---|---|---|---|
1011 0101 | 101 0 0101 | ||
& | 0001 0000 | & | 000 1 0000 |
0001 0000 | 000 0 0000 | ||
第五位結果 | 1 | 0 |
- 根據表格中計算過程可知,根據第五位的結果就能確定擴容後的索引
- 走到這裏,就引出了最後一個問題,影響結果的第五位如何確定呢?我們發現進行計算的二進制數0001 0000 = 16 ,正好就是擴容前的容量值,所以最後的結論就是:
- hash & oldcap = 0 擴容後索引不變
- hash & oldcap !=0 擴容後 索引= oldcap + 原索引值,這樣回頭再去看java中hashmap 擴容的原理源碼就一目瞭然了
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;//首次初始化後table爲Null
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;//默認構造器的情況下爲0
int newCap, newThr = 0;
if (oldCap > 0) {//table擴容過
//當前table容量大於最大值得時候返回當前table
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//table的容量乘以2,threshold的值也乘以2
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
//使用帶有初始容量的構造器時,table容量爲初始化得到的threshold
newCap = oldThr;
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) {
HashMap.Node<K,V> e;
if ((e = oldTab[j]) != null) {
// help gc
oldTab[j] = null;
if (e.next == null)
// 當前index沒有發生hash衝突,直接對2取模,即移位運算hash &(2^n -1)
// 擴容都是按照2的冪次方擴容,因此newCap = 2^n
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof HashMap.TreeNode)
// 當前index對應的節點爲紅黑樹,這裏篇幅比較長且需要了解其數據結構跟算法,因此不進行詳解,當樹的個數小於等於UNTREEIFY_THRESHOLD則轉成鏈表
((HashMap.TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
// 把當前index對應的鏈表分成兩個鏈表,減少擴容的遷移量
HashMap.Node<K,V> loHead = null, loTail = null;
HashMap.Node<K,V> hiHead = null, hiTail = null;
HashMap.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) {
// help gc
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
// help gc
hiTail.next = null;
// 擴容長度爲當前index位置+舊的容量
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
參考
https://www.jianshu.com/p/321fdf485970
https://blog.csdn.net/u010890358/article/details/80496144#擴容機制核心方法Node%3CK%2CV%3E%5B%5D%20resize()