1. 程式人生 > >帶你走進Java集合_HashMap原始碼分析_分析容器大小必須是2的整數次冪原因

帶你走進Java集合_HashMap原始碼分析_分析容器大小必須是2的整數次冪原因

我們上一篇文章主要介紹了HashMap的底層資料結構、構造方法、重要的屬性,在上一篇我們遺留了一個問題,那就是為什麼HashMap的大小必須是2的整數次冪,這一篇文章,我們從原始碼的角度來解決這個問題。首先我們回顧一下上一篇文章的重點內容

1)HashMap的底層資料結構是陣列+連結串列+紅黑樹,我將要有一篇文章重點講解HashMap的連結串列、紅黑樹。

2)底層陣列的容量大小必須是2的整數次冪。這篇文章重點講解

3)擴容相關的兩個重要屬性loadFactor(載入因子)和threshold(閥門),其中threshold=底層陣列容量大小*loadFactor;

4)HashMap建構函式中並沒有初始化底層陣列的大小,底層陣列的大小是第一次呼叫put時初始化的。

講解這個知識點之前,我們要知道.

1)呼叫HashMap無參建構函式HashMap(),底層陣列的大小為1<<4,也就是16,2的4次冪

2)呼叫HashMap有參建構函式HashMap(int initialCapacity) 通過tabSizeFor計算得到大於或等於initialCapacity最接近的2的整數次冪。

例如:使用者HashMap(8).通過tabSizeFor方法得到8,HashMap(7)通過tabSizeFor方法得到8,HashMap(9)通過tabSizeFor方法得到16.其中tabSizeFor的第一行程式碼cap-1的作用就為了防止使用者給定的就是2的整數次冪,上面8,如果沒有cap-1,通過下面的計算獲得了16.實際上應該是8,所以知道了cap-1的作用了。

綜上所述,不管是預設的,還是我們指定的大小,最終底層陣列容器的大小一定為2的整數次冪,這是為什麼呢?我們接下來從原始碼解讀分析。

第一步:我們要從put的方法說起,HashMap的put原始碼如下:

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

我們首先看hash(key),HashMap為了效能的,利用Hash雜湊儲存的,但是不管hash演算法如何的好,都有可能出現hash衝突,HashMap利用key的hash值的高16位與低16位進行異或來降低hash的衝突。原始碼如下:

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

異或的運算規律:兩個運算元,相同則為0,不同則為1,舉例說明:

1)2^3=1


2)9^17=24


HashMap通過key的hash值的高16位和低16位異或運算,是基於時間、效率多方的考量。

我們在進入putVal()方法

p = tab[i = (n - 1) & hash]

邏輯與的運算規則:兩個運算元,全1則1,否則為0

putVal其他的我們暫且不分析,就分析其中上面的程式碼(n-1)&hash,找出下標,加入我們預設容器16,當put值時,雜湊的越分散越好,最好的情況就是沒有碰到hash衝突,而(n-1)&hash儘可能的會均勻分佈,我們上面知道,n一定是2的整數次冪。我們接下來舉例說明:

如果我們預設底層陣列的大小n=16,計算出的hash值分別1,2,3,4,5,6.......12,通過(n-1)&hash的結果如下:


可以從以上的結果中看得出,通過(n-1)&hash可以均勻的分佈。

如果n不是2的整數次冪,n=20,計算出的hash值分別1,2,3,4,5,6.......12   (n-1)&hash


通過的上面的列子可以看出,如果n不是2的整數次冪,(n-1)&hash分佈的很不均勻,會導致計算出的下標衝突,形成連結串列或者紅黑樹,導致效能下降,有的同學會有兩個疑問?

疑問1:為什麼要n-1呢?

我們上面證明了n必須是2的整數次冪,如果直接利用n&hash來計算的話,n轉換成二進位制,只有最高位為1,其餘位數都為0,邏輯與運算&規律:全1則1,否則為0,也是為了避免分佈不均的情況。

疑問2:獲取陣列的下標,為什麼不用取餘運算呢? hash%n

邏輯與運算的效能要高於取模運算,實際上HashMap中的(n-1)&hash的功能和取模運算的功能相同。

通過上面的講解,我們是不是知道了為什麼HashMap的底層容器的大小必須是2的整數次冪呢,現在我們總結一下:

1)HashMap通過key的高16位與key的低16位異或運算key.hashCode()^(key.hashCode>>>16),這樣是基於從時間、效率等綜合方面考慮的。

2)HashMap的底層容器大小必須是2的整數次冪,因為(n-1)&hash分佈的更均勻,而(n-1)&hash是獲取底層陣列的下標,通過(n-1)&hash,減少衝突。只有是2的整數次冪,(n-1)&hash才能起到很好的作用。

3)(n-1)&hash和取餘(%)運算達到同樣的效果,但是前者效率遠高於後者。

4)徹底理解底層陣列容器的大小必須是2的整數次冪的原始碼(n-1)&hash進行分析的