【HashMap】爲什麼長度總是2的整數次方

前言

開門見山,HashMap這樣做有兩點原因

  1. 提升計算效率,更快算出元素的位置
  2. 減少哈希碰撞,使得元素分佈均勻

提升計算效率

我們先看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);
    }

其中(h = key.hashCode()) ^ (h >>> 16)暫時不需要理解,會在文尾進行分析,這裏只需要知道hash()方法返回二次運算後的哈希值即可。

接着回到putVal()方法中:

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

但對於機器而言,位運算永遠比取餘運算快得多,在length爲2的整數次方的情況下,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的整數次方時,哈希碰撞的概率起碼能下降一半,而且所有元素也能均勻地分佈在數組上


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

可能很多人都不知道這段代碼的作用,下面我談談自己的想法。

首先需要理解>>>的含義,即無符號右移,高位補0。例如:

0111 1110 0000 1111 1011 0001 0101 1010

右移16位得到

0000 0000 0000 0000 0111 1110 0000 1111

爲什麼要使用這個運算呢,直接返回key的哈希值不好嗎?

一般來說,任意一個對象的哈希值比較大,隨便實例化一個對象,得到它的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。

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章