之前有對HashMap的核心源碼put流程進行過分析,今天來分析一下resize流程以及移動元素時計算索引的原理。
源碼分析
這裏對resize的主要流程進行分析,主要結合註釋和流程圖進行理解。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
//當oldTab爲null時設置oldCap舊容量爲0;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;//引用原有臨界值
int newCap, newThr = 0;//初始設置新的容量和新的擴容上限爲0
if (oldCap > 0) {
//超過最大值就不進行擴容操作,只能任隨其進行碰撞
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//沒有超過就擴容爲原來的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//當原有臨界值大於0時表示已經初始化過了
else if (oldThr > 0) // initial capacity was placed in threshold
//如果已經初始過則將舊的擴容上限設置爲新的容量
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//臨界值<=0表示還沒有進行初始化,使用默認的初始容量進行初始化
newCap = DEFAULT_INITIAL_CAPACITY;//16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//12
}
//計算新的resize上線
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"})
//根據newCap進行生成table
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//16大小的數組
table = newTab;//將新的table賦值給table屬性
//將oldTab中的值賦值給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)
//當前索引位置沒有後續的節點,則直接將當前節點放入新的table對應的索引位置
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;
//這個地方很巧秒,通過e.hash & oldCap來得出newTab中的位置,
//因爲table是2倍擴容,所以只需要看hash值與oldCap進行操作,結果爲0,那麼還是原來的index;否則index = index + oldCap
//至於爲什麼這樣計算就可以判斷索引位置我們後面具體分析
if ((e.hash & oldCap) == 0) {
//對lo鏈表添加新的節點
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
//對hi鏈表添加新的節點
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//即e.hash & oldCap == 0的鏈表索, 引不會發生變化, 直接放入newTable中 原索引 位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//即e.hash & oldCap != 0的鏈表, 索引會發生變化, 在原索引 + oldCap位置, 放到newTable中
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
resize流程圖:如果大圖不清晰的話可以通過processOn查看
索引計算原理
在遍歷oldTab上的鏈表時進行了 (e.hash & oldCap) == 0 的判斷,如果成立則索引位置不變,如果不成立則新的索引位置爲 :
index = index + oldCap,如上面流程圖中最右邊部分的循環圖示。
接下來我們就來證明一下爲什麼上面的計算方式是正確的,首先明白三個計算原則:
1.table的容量始終是2^x,每次擴容都是擴容爲原有的2倍(特殊情已經在流程中過濾)
2.hash的運算:在HashMap中是使用了移位運算,h = key.hashCode()) ^ (h >>> 16), 即高16位和低16位進行亦或運算(“>>>”符號是無符號右移,忽略符號位,空位都以0補齊)
3.索引的計算公式爲(n - 1) & hash,相當於取模運算 hash % n (注:這裏的n爲table的容量,具體原理可以參考博客)
對於二進制運算在這裏我們就用符號"..."代替不重要的低位上的數了,高位我們用0補齊。
這裏我們設置 oldCap = 2^x,我們用二進制表示 oldCap = 00000000 1(第x+1位) 000....000 (1後面一共x個0,即低x位都爲0)
根據第三條計算原帶入x計算原索引:index = (2^x - 1) & hash
計算結果:index = 00000000 0(第x + 1位) ..........(低x位) ;
那麼 newCap = oldCap * 2 = 2^(x +1),
擴容後新索引爲:newIndex = (2^(x + 1) - 1) & hash
則索引結果無非就是兩種:在二進制中的 x+1 位不是0就是1
第一種結果:newIndex = 0000000 0(第x + 2位) 1 (第x + 1位) .........(低x位)
第二種結果:newIndex = 0000000 0(第x + 2位) 0 (第x + 1位) .........(低x位)
到這裏我們就可以知道如果是第 x + 1 位是0那麼和index的值就是一樣的。如果第 x + 1 位是1那麼得到:
newIdex = index + oldCap
即:0000000 0(第x + 2位) 1 (第x + 1位) .........(低x位) = 00000000 0(第x + 1位) ..........(低x位) + 00000000 1(第x+1位) 000....000
所以得到必然的結果就是新索引要麼與原索引相同要麼就是原索引加上原容量值得到新索引。
但是這還不足以解釋爲什麼 (e.hash & oldCap) == 0 就可以判斷新索引與原索引不變。
細心點你可以發現(e.hash & oldCap) = hash & 2^x,這個式子相當於 hash & 0000000 1(第x+1位) 000....000 (1後面一共x個0,即低x位都爲0)
這裏可以發現和求新索引的計算式很像: hash & (2^(x+1) -1),實際上就是將後面的低x位全部換成0,只需關心第x+1位進行hash運算後的值。這裏我們得到了我們想要的結論:
hash & oldCap = 0,代表的就是第x + 1位上的值位0,hash & oldCap = 1,代表x + 1位上的值爲1。
再根據前面的結論: x + 1 位爲0時,newIndex = index;x + 1位爲1時, newIndex = index + oldCap。
所以最後得出: (e.hash & oldCap) == 0,newIndex = index;(e.hash & oldCap) != 0, newIndex = index + oldCap