HashMap淺析(二)
擴容機制
首先上擴容的代碼,每一行都寫了註釋,儘可能的讓大家明白這個過程。
final Node<K,V>[] resize() {
//將table賦值給oldTab
Node<K,V>[] oldTab = table;
//聲明oldTab的容量給oldCap
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//擴容閾值賦值給oldThr
int oldThr = threshold;
//上面這三行代碼,其實就是爲了區分出擴容前與擴容後的變量,衆所周知,數組無法動態擴容
//聲明新數組的容量和擴容閾值,暫時爲0
int newCap, newThr = 0;
//如果舊數組的容量大於0
if (oldCap > 0) {
//如果舊數組的容量大於等於最大值,即1<<30
if (oldCap >= MAXIMUM_CAPACITY) {
//全局變量的擴容閾值賦值爲int的最大值,同時返回舊數組
threshold = Integer.MAX_VALUE;
return oldTab;
}
//如果新數組的容量(舊數組容量*2)小於最大容量,且,舊數組容量大於等於默認值1<<4
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//新數組的擴容閾值等於舊數組擴容閾值的2倍
newThr = oldThr << 1; // double threshold
}
//如果舊數組的擴容閾值大於0
else if (oldThr > 0) // initial capacity was placed in threshold
//新數組的擴容閾值就等於舊數組的擴容閾值
newCap = oldThr;
//如果既不走oldCap>0,也不走oldThr>0的分支,那麼走else
else { // zero initial threshold signifies using defaults
//新數組的容量爲默認容量:16
newCap = DEFAULT_INITIAL_CAPACITY;
//新數組的擴容閾值=默認載荷因素*默認初始容量=12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//新的if分支:如果新數組的擴容閾值等於0
if (newThr == 0) {
//ft=新數組容量*0.75,loadFactory在用戶未指定時,就是默認的0.75
float ft = (float)newCap * loadFactor;
//當新數組的容量小於最大容量,且ft小於最大容量時,新數組的擴容閾值就爲ft,
//否則爲int的最大值
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//將新得到的新數組的擴容閾值,賦值給全局變量的擴容閾值
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//創建一個長度爲newCap的node數組
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//並讓全局變量的table數組指向新數組
table = newTab;
//如果舊數組不等於空
if (oldTab != null) {
//循環舊數組
for (int j = 0; j < oldCap; ++j) {
//聲明一個Node對象,用來承載舊值
Node<K,V> e;
//將數組的j角標位置的值賦值給e,同時判斷是否爲null
if ((e = oldTab[j]) != null) {
//不爲空,則把原數組的j位置重新賦值爲null
//當循環完畢,舊數組就是空數組,方便回收
oldTab[j] = null;
//判斷當前數組元素e的鏈表的下一個結點是否爲null
if (e.next == null)
//證明當前數組元素只有一個結點,所以只需在新數組中放置e元素
newTab[e.hash & (newCap - 1)] = e;
//首先走到else if分支這裏,已經確定e.next不爲null
//那麼就需要判斷是紅黑樹還是鏈表
else if (e instanceof TreeNode)
//紅黑樹部分先過掉
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//確定爲鏈表之後
else { // preserve order
//創建五個node對象
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
//注意是do-while循環,即先執行一次,再走while
do {
//next即爲當前遍歷到的元素的下一個結點
next = e.next;
//如果當前元素的hash&舊數組容量的值爲0
//當你看到這裏時,請先看下一節:擴容中鏈表複製的詳解
//便於理解之後的流程
//判斷角標是否需要修改,下面的分析會單獨拿出來,因爲涉及到循環和判斷
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;
}
do-while中具體執行流程
現在假設當前元素e的鏈表中存在三個結點,a、b、c分別爲三個結點的內存地址,假設e不需要重新計算角標
也就是e ->a, e.next->b ,e.next.next->c
第一次執行
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
//第一次執行,next->b,e->a
next = e.next;
//判斷元素位置是否改變
if ((e.hash & oldCap) == 0) {
//不需要改變走這個分支
//判斷loTail是否爲null,現在是第一次執行,肯定是null,進入if分支
if (loTail == null)
//loHead->a
loHead = e;
else
loTail.next = e;
//loTail->a
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
//然後執行判斷的同時,將e->next,e->b
} while ((e = next) != null);
用個圖表示第一次執行之後,變量的關係
第二次循環
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
//next->c,e->b
next = e.next;
if ((e.hash & oldCap) == 0) {
//loTail->a,所以走else
if (loTail == null)
loHead = e;
else
//loTail.next->b
loTail.next = e;
//loTail->b
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
//然後執行判斷的同時,將e->next,e->c
} while ((e = next) != null);
第三次循環
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
//next->null,e.next->null,e->c
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
//loTail.next->c
loTail.next = e;
//loTail->c
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
//e=next == null,while循環結束
} while ((e = next) != null);
跳出循環之後的處理
//loTail不爲null,因爲loTail指向的是a的內存地址,而a又是鏈表的頭結點
//所以只需要將新數組的j位置元素指向loHead的地址,最終也就是指向的a
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//這個方法是針對需要重新計算元素下角標的元素
if (hiTail != null) {
hiTail.next = null;
//針對爲何是j+oldCap可以查看下面的內容
newTab[j + oldCap] = hiHead;
}
擴容中鏈表複製的詳解
在resize()方法中,針對鏈表的複製,HashMap採取了一個非常有意思的方法。
它沒有針對新數組重新計算hash,從而確定下角標,而是通過e.hash & oldCap判斷元素的前後位置是否有變化。
-
假設當前元素的hash爲10:1010(二進制),數組擴容前爲8:1000(二進制),
那麼計算角標的方法就是(8-1)&10,index=2
-
擴容後的下角標,hash爲10:1010(二進制),數組擴容後爲16:0001 0000(二進制),
那麼計算角標的方法就是(16-1)&10,index=10
大家仔細看,其實擴容就是高一位從0到1,這時如果確定hash的對應位是0還是1,就可以確定,元素在新數組的位置是否有變化了。
那麼如何確定呢,e.hash & oldCap就是用來判斷這個變化的。
首先&的運算,就保證瞭如果兩個二進制的對應位都是1,計算出來的值的對應位才爲1.
由上圖可知,e.hash & oldCap!=0,所以需要重新計算位置。
通過下圖,你能發現什麼問題呢?
新舊座標的差值其實就是數組擴容前後,區分出來的高一位1,然後又因爲數組擴容是嚴格按照2的冪次方進行的,所以必然存在:
當e.hash & oldCap!=0時,
新數組的下角標位置=舊數組的下角標+2的m次方(m=圖中容量擴容前後-1的二進制碼中的發生變化的位置)
而
所以有
如果還不夠明白的話,我繼續通過推導e.hash & oldCap==0的情況給各位再反向操作一波。
爲了方便推導,我將其中的hash值修改了一下,下圖是擴容前後求index的圖。
下圖可知:index值在擴容前後沒有發生變化。
下圖是e.hash & oldCap的計算圖,對應圖中標註的1.2.3,依次做一個解釋。
1.因爲容量是嚴格按照2的冪次方擴容,所以整段二進制碼中,只存在一個1;
2.兩者進行"&"計算後,如果hash的對應位(指容量的二進制碼中1的位置)爲0,那麼結果必然爲0.
3.反之亦然。
當擴容後的容量爲16, 0001 0000,
所對應的n-1,即16-1, 0000 [1]111,
舊容量的n-1,即8-1, 0000 [0]111,
二者的區別就在[]號標識的那一位,所以利用上面2.的結論,無論是擴容多少次,滿足e.hash & oldCap==0的條件下,角標都不會變