前言
開門見山,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);
}
其中(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。
區別兩個細微對象的不同,就要深挖其細微之處。因此要在最大程度上避免哈希衝突,就越要使用到所有已知的特徵,不能認爲細微就沒用。