為啥HashMap的長度一定是2的n次方
首先你應當記住的: 不管你傳不傳引數,不管你傳入的長度為多少,在你用HashMap的時候,他的長度都是2的n次方,且最大長度為2的30次方
最大長度
在HashMap的原始碼中,最大長度這個常量值是這樣定義的
/** * The maximum capacity, used if a higher value is implicitly specified * by either of the constructors with arguments. * MUST be a power of two <= 1<<30. */ static final int MAXIMUM_CAPACITY = 1 << 30; 複製程式碼
這個值用在哪裡呢?
- resize()函式,這個是用來擴容的
- tableSizeFor(),這個也是用來擴容的
- 建構函式中
- putEntries(),存放一組HashMap元素時,不是存放單個
為什麼table長度一定是2的n次方
注意,原始碼中他們採用了 延遲初始化操作 ,也就是table只有在用到的時候才初始化,如果你不對他進行 put
等操作的話,table的長度永遠為"零"
主要有兩個函式保證了他的長度為2的n次方
- tableSizeFor()
- resize()
至於計算過程以及載入過程,請參考我的這篇文章: table的長度到底是多少
這篇文章我從原始碼分析table的建立過程,包括上面提到的函式的呼叫,看了這個你一定明白為啥 table
的長度一定是2的n次方
當然我針對hashMap寫的一部分原始碼的中文註釋github上也有: HashMap原始碼中文註釋
2的n次有什麼好處
- 計算方便
- hash分佈更均勻
分佈均勻
如果不是2的n次方,那麼有些位置上是永遠不會被用到
具體可以參考這篇博文,他用例子講述了為什麼,為啥長度要是2的n次方
計算方便
-
當容量一定是2^n時,h & (length - 1) == h % length
-
擴容後計算新位置,非常方便,相比 JDK1.7
JDK 1.8改動
在 JDK1.8 中,HashMap有了挺大的改動,包括
-
元素遷移演算法(舊的到新的陣列)
-
使用紅黑樹
-
連結串列為尾插法
其中我重點講下元素遷移演算法,JDK1.8的時候
首先看下java程式碼以及我的註釋,如果要看完整的, 可以看我的github倉庫
// 將原來陣列中的所有元素都 copy進新的陣列 if(oldTab != null){ for (int j = 0; j < oldCap; j++) { Entry e; if((e = oldTab[j]) != null){ oldTab[j] = null; // 說明還沒有成鏈,陣列上只有一個 if(e.next == null){ // 重新計算 陣列索引 值 newTable[e.h & (newCap-1)] = e; } // 判斷是否為樹結構 //else if (e instanceof TreeNode) // 如果不是樹,只是連結串列,即長度還沒有大於 8 進化成樹 else{ // 擴容後,如果元素的 index 還是原來的。就使用這個lo字首的 Entry loHead=null, loTail =null; // 擴容後元素index改變,那麼就使用 hi字首開頭的 Entry hiHead = null, hiTail = null; Entry next; do { next = e.next; //這個非常重要,也比較難懂, // 將它和原來的長度進行相與,就是判斷他的原來的hash的上一個bit 位是否為 1。 //以此來判斷他是在相同的索引還是table長度加上原來的索引 if((e.h & oldCap) == 0){ // 如果 loTail == null ,說明這個 位置上是第一次新增,沒有雜湊衝突 if(loTail == null) loHead = e; else loTail.next = e; loTail = e; } else{ if(hiTail == null) loHead = e; else hiTail.next = e; hiTail = e ; } }while ((e = next) != null); if(loTail != null){ loTail.next = null; newTable[j] = loHead; } // 新的index 等於原來的 index+oldCap else { hiTail.next = null; newTable[j+oldCap] = hiHead; } } } } } 複製程式碼
我們看到上面原始碼的最後一句, newTable[j+oldCap] = hiHead;
意思就是哪怕我們的元素從舊的陣列遷移到新的陣列,我們也不需要重新計算他的hash和新陣列長度相與的值,只需要直接將現在的 索引值+原來陣列的長度
即可

藍色的表示不需要移動的,綠色的表示需要重新計算索引的,我們看到,他只是加了16(原來的陣列table長度)
計算索引需要
我們注意到上面的原始碼中,判斷擴容後元素位置需不需要改變的時候,我們使用到了這個判斷
if((e.h & oldCap) == 0)
,
如果為0,那麼就不需要改變,使用舊的索引即可;如果為1,那麼就需要使用新的索引
為啥會這樣呢?
- 如果元素的索引要變那麼
hash&(newTable.length-1)
一定是和hash&(oldTable.length-1)+oldTable.length
相等 - 因為table的長度一定是2的n次方,也就是oldCap 一定是2的n次方,也就是說 oldCap有且只有一位是1,而且這個位置在最高位;
我們來舉個例子:
我們假設元素的hash值的後12位是 110111010111,陣列原來的長度為16,擴容後陣列長度為32

你可以試下下次擴容時,擴容到64時,索引變不變化。當然答案是不會變化,因為元素的hash值在那個位置為 0
對比1.7擴容
我們來對比JDK1.7 的方式,他如果要擴容,並且擴容後計算元素的索引的話要使用 indexFor函式
/** * Returns index for hash code h. */ static int indexFor(int h, int length) { // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2"; return h & (length-1); } 複製程式碼
也就是要把元素的hash值重新再和新的陣列長度-1 再相與一次,會比較麻煩而且不優雅,完全沒有我看到1.8計算方式的那種驚豔感。