java HashMap hash方法分析

下面分別分析下,JDK1.7 與 JDK1.8 中 hash方法的運算過程,並且左後結合JDK1.8 中 hash方法來進行詳細說明。

 

JDK1.7 中HashMap 中hash table 定位算法: 

int hash = hash(key.hashCode()); 

int i = indexFor(hash, table.length);  


其中indexFor和hash源碼如下: 

/** 
* Applies a supplemental hash function to a given hashCode, which 
* defends against poor quality hash functions. This is critical 
* because HashMap uses power-of-two length hash tables, that 
* otherwise encounter collisions for hashCodes that do not differ 
* in lower bits. Note: Null keys always map to hash 0, thus index 0. 
*/ 
static int hash(int h) { 
// This function ensures that hashCodes that differ only by 
// constant multiples at each bit position have a bounded 
// number of collisions (approximately 8 at default load factor). 
	h ^= (h >>> 20) ^ (h >>> 12); 
	return h ^ (h >>> 7) ^ (h >>> 4); 
} 

/** 
* Returns index for hash code h. 
*/ 
static int indexFor(int h, int length) { 
	return h & (length-1); 
}

indexFor這個方法論壇中已有人分析過,這裏就不再分析。 

現在分析一下hash算法: 

Java代碼  

h ^= (h >>> 20) ^ (h >>> 12);  

return h ^ (h >>> 7) ^ (h >>> 4);  


假設key.hashCode()的值爲:0x7FFFFFFF,table.length爲默認值16。 
上面算法執行如下: 

 

得到i=15 

其中h^(h>>>7)^(h>>>4) 結果中的位運行標識是把h>>>7 換成 h>>>8來看。 

即最後h^(h>>>8)^(h>>>4) 運算後hashCode值每位數值如下: 
8=8 
7=7^8 
6=6^7^8 
5=5^8^7^6 
4=4^7^6^5^8 
3=3^8^6^5^8^4^7 
2=2^7^5^4^7^3^8^6 
1=1^6^4^3^8^6^2^7^5 
結果中的1、2、3三位出現重複位^運算 
3=3^8^6^5^8^4^7     ->   3^6^5^4^7 
2=2^7^5^4^7^3^8^6   ->   2^5^4^3^8^6 
1=1^6^4^3^8^6^2^7^5 ->   1^4^3^8^2^7^5 

算法中是採用(h>>>7)而不是(h>>>8)的算法,應該是考慮1、2、3三位出現重複位^運算的情況。使得最低位上原hashCode的8位都參與了^運算,所以在table.length爲默認值16的情況下面,hashCode任意位的變化基本都能反應到最終hash table 定位算法中,這種情況下只有原hashCode第3位高1位變化不會反應到結果中,即:0x7FFFF7FF的i=15。

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

JDK1.8中hash方法已經得到了簡化

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

並且針對HashMap中的hash算法,大家看的一臉懵逼。這段代碼叫“擾動函數”。

我們知道,HashMap通過 (length - 1) & hash 的方式獲取table數組的下標,並且table的長度必須爲2 ^ n,因爲這樣前面的公式就相當於取模。

如果我們對key的hash不進行擾動,“與”操作的結果就是hashcode的高位全部歸0,只保留低位的值用來做數組下標訪問。以初始長度16爲例,16 - 1 = 15。2進製表示是00000000 00000000 00001111。和某散列值做“與”操作如下,結果就是借去了最低位的四位。

    10100101 11000100 00100101

&  00000000 00000000 00001111

---------------------------------------------

    00000000 00000000 00000101    // 高位全部歸0,只保留末四位

但這個時候問題就來了,這樣就算我的hashcode散列值再怎麼鬆散,要是隻取最後幾位的話,碰撞也會很嚴重。更要命的是如果散列本身做的不好,分佈上成等差數列的漏洞,恰好使最後幾個低位呈現規律性重複,就無比蛋疼。

這時候擾動函數的價值就體現出來了,我們根據圖來分析JDK1.8 的hash方法如何擾動的

右移16位,正好是32bit的一半,自己的高半區和低半區作異或,就是混合原始哈希嗎的高位和低位,以此來加大低位的隨機性。而混合後的低位can澤勒高位的部分特徵,這樣高位的信息也被表象保留下來。

最後我們來看一下Peter Lawley的一片專欄文章《An introduction to optimising a hashing strategy》裏的一個實驗:他隨機選取了352個字符串,在他們散列值完全沒有衝突的前提下,對他們做低位掩碼,取數組下標。

結果顯示,當HashMap數組長度爲512的時候,也就是用掩碼取低九位的時候,在沒有擾動函數的情況下,發生了103次碰撞,接近30%。而在使用了擾動函數後只有92次碰撞。碰撞減少了10%。看來擾動函數確實有功效的。

但明顯Java8 覺得擾動做一次就夠了,做4次的話,多了可能邊際效用也不大,所謂爲了效率就改成了一次。

那爲什麼必須要用 ^ 做擾動呢?

因爲異或運算,實現均衡分配得到1或者0的概率都是1/2,而&(與)運算得到0的概率較大爲75%,| (或)運算得到1的概率較大爲75%。下面舉例:

對於0/1的運算組合無非就是下面四種,我們分別做異或、與、或運算

    0   |   1   |   1   |   0

^  1   |   0   |   1   |   0 

---------------------------

    1       1       0       0

 

    0   |   1   |   1   |   0

&  1   |   0   |   1   |   0 

---------------------------

    0       0       1       0

 

    0   |   1   |   1   |   0

|   1   |   0   |   1   |   0 

---------------------------

    1       1       1       0

我們可以看到,對於不同情況下的運算,只有異或運算表現出結果均勻,這也是爲什麼擾動運算要用異或的原因了

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