1. 程式人生 > 其它 >HashMap雜湊函式分析

HashMap雜湊函式分析

首先我們要知道,在理想情況下的雜湊表中,雜湊函式生成的雜湊值是value在陣列中的下標,其範圍是分佈於負無窮到正無窮的整個實整數軸的。而在現實情況下,是不可能存在這麼大的一個數組的。接下來分析HashMap怎麼處理:
HashMap的put方法:

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

put方法使用的不是Object提供的key.hashcode(),而是hash(key):

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

在key!=0的情況下,進行一下拆解分析:

static final int hash(Object key) {
    int h = key.hashCode();
    int l = h>>>16;
    return h^l;
}

先取Object.hashcode(),是32位;然後右移16位,將低16位丟棄;將hashCode的低16位與高16位進行按位異或運算然後返回。
這就是擾動函式,擾動函式是如何減少衝突的?
由開頭的分析,我們知道HashMap是不可能使用直接的雜湊值的,因為不可能一個HashMap就要分配無限大(或者2^32次方大)的陣列空間。
因此實際上HashMap是將雜湊值對當前陣列長度取餘:

//原始碼部分擷取
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);

看tab[i = (n - 1) & hash]這裡,HashMap在陣列中的實際下標其實是 (陣列長度-1)&hash,其實就是hash%陣列長度。
以初始長度16為例,一個雜湊值分佈於整個實整數軸,取餘16之後,必然分佈於[0,15]區間範圍內,也就無需去分配無限大的陣列空間了。
這樣做有什麼問題呢?
一個好的雜湊函式,要做到生成的雜湊值足夠分散。但是對陣列長度取餘後,相當於只截取了低位(因為HashMap的容量總是16的整數倍)。
如果一個key的雜湊值的低四位是0010,那麼在取餘16之後,就只剩下0010,也就是十進位制2。
雜湊函式可能設計得在低位不是那麼地隨機,那麼只保留低位的效果,就相當於完全拋棄了高位的隨機性,因此需要這樣的擾動函式,將高位與低位進行運算,增強低位的隨機性。
在這篇文章中《An introduction to optimising a hashing strategy》,對比發現,採用高位擾動低位的方式進行hash,會使得雜湊衝突減少10%。
順便分析一下為什麼HashMap的容量總是2的冪次方
首先HashMap的初始容量是16,隨後每當實際容量佔到了擴容因子*最大容量後,容量擴大為當前的兩倍。因此HashMap的容量總是16*2的冪次方。
之前說得hashcode取餘數組長度,只有在陣列長度為2的冪次方的情況下,才可以轉為(n - 1) & hash的位運算,從而提高運算效率。