概念
HashMap
是Java Collections Framework
中Map
集合的一種實現。HashMap
提供了一種簡單實用的資料儲存和讀取方式。Map
介面不同於List
介面,屬於集合框架的另一條支線,Map
提供了鍵值對K-V
資料儲存模型,底層則是通過Hash
表儲存。
本文分析基於JDK1.8
。
類結構
HashMap
實現了Map
介面,Map
介面設定一系列操作Map
集合的方法,如:put
、get
、remove
…等方法,而HashMap
也針對此有其自身對應的實現。
HashMap
繼承AbstractMap
類。AbstractMap
類對於Map
介面做了基礎的實現,實現了containsKey
、containsValue
…等方法。
類成員
建構函式
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
;
threshold
是HashMap
判斷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 >>> 1
,n
無符號右移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
如何保證cap
為2
的冪次方已經顯而易見了。那麼問題來了,為什麼cap
要保持為2
的冪次方?
為什麼cap要保持為2的冪次方?
cap
要保持為2的冪次方主要原因是HashMap
中資料儲存有關。
在JDK1.8
中,HashMap
中key
的Hash
值由Hash(key)
方法(後面會詳細分析)計算得來。
HashMap
中儲存資料table
的index
是由key
的Hash
值決定的。在HashMap
儲存資料的時候,我們期望資料能夠均勻分佈,以避免雜湊衝突。自然而然我們就會想到去用%
取餘的操作來實現我們這一構想。
這裡要了解到一個知識:取餘(%
)操作中如果除數是2的冪次方則等同於與其除數減一的與(&
)操作。
這也就解釋了為啥一定要求cap
要為2
的冪次方。再來看看table
的index
的計算規則:
index = e.hash & (newCap - 1)
等同於:
index = e.hash % newCap複製程式碼
採用二進位制位操作&
,相對於%
,能夠提高運算效率,這就是要求cap
的值被要求為2
冪次方的原因。