1. 程式人生 > 其它 >HashMap長度2次冪/擴容相關

HashMap長度2次冪/擴容相關

HashMap這樣做有兩點原因

  提升計算效率,更快算出元素的位置
  減少雜湊碰撞,使得元素分佈均勻


提升計算效率
我們先看put方法的細節:

public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

其中hash(key)如下:

static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}


hash()方法返回二次運算後的雜湊值即可。

putVal()方法:

可以看到,對於一個key,拿到hash(key)後,之後要確定這個key在陣列中的位置,我們一般傾向於對陣列長度length取餘,餘數是幾,就在陣列的第幾個位置上,簡單方便。

但對於機器而言,位運算永遠比取餘運算快得多,在length為2的整數次方的情況下(length-1得到的二進位制為全1),hash(key)%length能被替換成高效的hash(key)&(length-1),兩者的結果是相等的。


減少雜湊碰撞
如果陣列長度是2的整數次方時,也就是一個偶數,length-1就是一個奇數,奇數的二進位制最後一位是1,因此不管hash(key)是多少,hash(key)&(length-1)的二進位制最後一位可能是0,也可能是1,取決於hash(key)。即如果hash(key)是奇數的話,則對映到陣列上第奇數個位置上。

如果length是一個奇數的話,length-1就是一個偶數,偶數的二進位制最後一位是0,則不管hash(key)是奇數還是偶數,該元素都會被對映到陣列上第偶數個位置上,奇數位置上沒有任何元素!

因此,陣列長度是一個2的整數次方時,雜湊碰撞的概率起碼能下降一半,而且所有元素也能均勻地分佈在陣列上。


JDK8的擴容機制

HashMap的容量變化通常存在以下幾種情況:

  1. 空引數的建構函式:例項化的HashMap預設內部陣列是null,即沒有例項化。第一次呼叫put方法時,則會開始第一次初始化擴容,長度為16
  2. 有參建構函式:用於指定容量。會根據指定的正整數找到不小於指定容量的2的冪數,將這個數設定賦值給閾值(threshold)。第一次呼叫put方法時,會將閾值賦值給容量,然後讓。(因此並不是我們手動指定了容量就一定不會觸發擴容,超過閾值後一樣會擴容!!)
  3. 之後超過閾值的擴容,容量變為原來的2倍,閾值也變為原來的2倍。(容量和閾值都變為原來的2倍時,負載因子還是不變)

JDK8的元素遷移

JDK8則因為巧妙的設計,效能有了大大的提升:由於陣列的容量是以2的冪次方擴容的,那麼一個Entity在擴容時,新的位置要麼在原位置,要麼在原長度+原位置的位置。原因如下圖:

陣列長度變為原來的2倍,表現在二進位制上就是多了一個高位參與陣列下標確定。此時,一個元素通過hash轉換座標的方法計算後,恰好出現一個現象:最高位是0則座標不變,最高位是1則座標變為“10000+原座標”,即“原長度+原座標”。如下圖:

因此,在擴容時,不需要重新計算元素的hash了,只需要判斷最高位是1還是0就好了。JDK8在遷移元素時是正序的,不會出現連結串列轉置的發生。


其他

key.hashCode()^ (h >>> 16)


>>>的含義,即無符號右移,高位補0。例如:

0111 1110 0000 1111 1011 0001 0101 1010
右移16位得到
0000 0000 0000 0000 0111 1110 0000 1111

一般來說,任意一個物件的雜湊值比較大,隨便例項化一個物件,得到它的hash值

轉換成2進位制後,得到

0011 1001 1010 0000 0101 0100 1010 0101
而HashMap的長度一般就在[1,2^16]左右,取length=16為例,那麼直接使用key的雜湊值&(length-1)後,得到

0011 1001 1010 0000 0101 0100 1010 0101
&
0000 0000 0000 0000 0000 0000 0000 1111
---------------------------------------
0000 0000 0000 0000 0000 0000 0000 0101
可見,運算後的結果對雜湊值的高位無效,即如果兩個不同物件的雜湊值僅僅在高位上不一樣的話,依然會存在雜湊衝突的情況。因此,我們現在打算讓運算後的結果對雜湊值的高位以及低位都有效。

而對雜湊值再次運算後,即使用key.hashCode()^ (h >>> 16)運算後,將雜湊值的低16位異或了高16位,讓高位與低位都影響到了對之後位置的選擇上。

為什麼使用^異或^能保證兩個數都能影響到最終的結果,而|中只要一個為1,不管對方是多少,結果都為1,&也是同樣的道理,有0則0。

區別兩個細微物件的不同,就要深挖其細微之處。因此要在最大程度上避免雜湊衝突,就越要使用到所有已知的特徵,不能認為細微就沒用。