原始碼分析之 HashMap

特立獨行的豬手發表於2019-03-04

概念

HashMapJava Collections FrameworkMap集合的一種實現。HashMap提供了一種簡單實用的資料儲存和讀取方式。Map介面不同於List介面,屬於集合框架的另一條支線,Map提供了鍵值對K-V資料儲存模型,底層則是通過Hash表儲存。

本文分析基於JDK1.8

類結構

原始碼分析之 HashMap

HashMap實現了Map介面,Map介面設定一系列操作Map集合的方法,如:putgetremove…等方法,而HashMap也針對此有其自身對應的實現。

HashMap繼承AbstractMap類。AbstractMap類對於Map介面做了基礎的實現,實現了containsKeycontainsValue…等方法。

類成員

建構函式

HashMap提供四種建構函式。最為基礎是如下這種:

HashMap(int initialCapacity, float loadFactor)
   public HashMap(int initialCapacity, float loadFactor) {

        ...        

        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }複製程式碼

HashMap(int initialCapacity, float loadFactor)是最基礎的建構函式。該建構函式提供兩個引數initialCapacity(初始大小)loadFactor(載入因子)

  • initialCapacity預設值是16 (1 << 4),最大值是 1073 741 824(1 << 30),且大小必須是小於最大值的2的冪次方;
  • loadFactor預設值是0.75,作用是擴容時使用;

初始化的過程中將傳入的引數loadFactor賦值給this.loadFactor,然後呼叫tableSizeFor(initialCapacity)方法將處理的結果值賦值給this.threshold;

thresholdHashMap判斷size是否需要擴容的閾值。這裡呼叫tableSizeFor(initialCapacity)來設定threshold;

tableSizeFor有啥用?

先丟擲答案:tableSizeFor方法保證函式返回值是大於等於給定引數initialCapacity最小的2的冪次方的數值。

如何實現?

 static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }複製程式碼

可以看出該方法是一系列的二進位制位操作。先說明 |=的作用:a |= b 等同於 a = a|b。逐行分析tableSizeFor方法:

  • int n = cap - 1

    • 給定的cap減1,是為了避免引數cap本來就是2的冪次方,這樣一來,經過後續的未操作的,cap將會變成2 * cap,是不符合我們預期的。
  • n |= n >>> 1

    • n >>> 1n無符號右移1位,即n二進位制最高位的1右移一位;
    • n | (n >>> 1),導致的結果是n二進位制的高2位值為1;

      目前n的高1~2位均為1

  • n |= n >>> 2

    • n繼續無符號右移2位。
    • n | (n >>> 2),導致n二進位制表示高3~4位經過運算值均為1

      目前n的高1~4位均為1

  • n |= n >>> 4

    • n繼續無符號右移4位。
    • n | (n >>> 4),導致n二進位制表示高5~8位經過運算值均為1

      目前n的高1~8位均為1

  • n |= n >>> 8

    • n繼續無符號右移8位。
    • n | (n >>> 8),導致n二進位制表示高9~16位經過運算值均為1

      目前n的高1~16位均為1

  • n |= n >>> 16

    • n繼續無符號右移16位。
    • n | (n >>> 16),導致n二進位制表示高17~32位經過運算值均為1

      目前n的高1~32位均為1

可以看出,無論給定cap(cap < MAXIMUM_CAPACITY )的值是多少,經過以上運算,其值的二進位制所有位都會是1。再將其加1,這時候這個值一定是2的冪次方。當然如果經過運算值大於MAXIMUM_CAPACITY,直接選用MAXIMUM_CAPACITY

這裡可以舉個例子,假設給定的cap的值為20

  • int n = cap - 1; —> n = 19(二進位制表示:0001 0011)

  • n |= n >>> 1;

    n             ->  0001 0011
    n >>> 1       ->  0000 1001
    n |= n >>> 1  ->  0001 1011複製程式碼
  • n |= n >>> 2;
    n             ->  0001 1011
    n >>> 2       ->  0000 1101
    n |= n >>> 2  ->  0001 1111複製程式碼

此時n所有位均為1,後續的位操作均不再改變n的值。

    n + 1        ->  0010 0000 (32)複製程式碼

最終,tableSizeFor(20)的結果為32(2^5)

至此tableSizeFor如何保證cap2的冪次方已經顯而易見了。那麼問題來了,為什麼cap要保持為2的冪次方?

為什麼cap要保持為2的冪次方?

cap要保持為2的冪次方主要原因是HashMap中資料儲存有關。

JDK1.8中,HashMapkeyHash值由Hash(key)方法(後面會詳細分析)計算得來。

HashMap中儲存資料tableindex是由keyHash值決定的。在HashMap儲存資料的時候,我們期望資料能夠均勻分佈,以避免雜湊衝突。自然而然我們就會想到去用%取餘的操作來實現我們這一構想。

這裡要了解到一個知識:取餘(%)操作中如果除數是2的冪次方則等同於與其除數減一的與(&)操作

這也就解釋了為啥一定要求cap要為2的冪次方。再來看看tableindex的計算規則:

 index = e.hash & (newCap - 1) 

 等同於:

 index = e.hash % newCap複製程式碼

採用二進位制位操作&,相對於%,能夠提高運算效率,這就是要求cap的值被要求為2冪次方的原因。

Node<k,v> 類

相關文章