來源聲明:本文是整理微信公衆號[程序員小灰]的漫畫系列文章
什麼是HashMap
HashMap是一個用於存儲Key-Value鍵值對的集合,每一個鍵值對也叫做Entry。這些鍵值對(Entry)分散存儲在一個數組中,這個數組就是HashMap的主幹。
HashMap數組每一個元素的初始值都是Null。
對於HashMap,我們最常使用的是兩個方法:Get和Put。
1.Put方法的原理
例如,調用hashMap.put(“apple”, 0),插入一個Key爲“apple”的元素。這時候我們需要利用一個哈希函數來確定Entry的插入位置(index):
index = Hash(“apple”)
假定最後計算出的index是2,那麼結果如下:
但是,因爲HashMap的長度是有限的,當插入的Entry越來越多時,再完美的Hash函數也難免會出現index衝突的情況。比如下面這樣:
這時候怎麼辦呢?我們可以利用鏈表來解決。
HashMap數組的每一個元素不止是一個Entry對象,也是一個鏈表的頭結點。每一個Entry對象通過Next指針指向它打的下一個Entry節點。當新來的Entry映射到衝突的數組位置時,只需要插入到對應的鏈表即可:
需要注意的是,新來的Entry節點插入鏈表時,使用的是“頭插法”。之所以用“頭插法”,是因爲HashMap的發明者認爲,後插入的Entry被查找的可能性更大。
###2.Get方法的原理
使用Get方法根據Key來查找Value的時候,發生了什麼呢?
首先會把輸入的Key做一次Hash映射,得到對應的index:
index = Hash(“apple”)
由於剛纔所說的Hash衝突,同一個位置有可能匹配到多個Entry,這時候就需要順着對應鏈表的頭節點,一個一個向下來查找。假設我們要查找的Key是“apple”:
第一步,我們查看的是頭節點Entry6,Entry6的Key是banana,顯然不是我們要找的結果。
第二步,我們查看的是Next節點Entry1,Entry1的Key是apple,正是我們要找的結果。
擴展
1、Hashmap的初始長度?爲什麼這麼定
HashMap的默認初始長度是16,並且每次自動擴展或是手動初始化時,長度必須是2的冪。
之所以選擇16,是爲了服務於從Key映射到index的Hash算法,實現一個儘量均勻分佈的Hash函數。
上面提到,從Key映射到HashMap數組的對應位置,會用到一個Hash函數:
index = Hash(“apple”)
如何實現一個儘量均勻分佈的Hash函數呢?我們通過利用Key的HashCode值來做某種運算,HashMap的發明者採用了位運算的方式。
如何進行位運算呢?有如下的公式(Length是HashMap的長度):
index = HashCode(Key) & (Length - 1)
舉例,以值爲“book”的Key來演示整個過程:
1、計算book的hashcode,結果爲十進制的3029737,二進制的101110001110101110 1001。
2、假定HashMap長度是默認的16,計算Length-1的結果爲十進制的15,二進制的1111。
3、把以上兩個結果做 與運算,101110001110101110 1001 & 1111 = 1001,十進制是9,所以 index=9。
可以說,Hash算法最終得到的index結果,完全取決於Key的HashCode值的最後幾位。
爲什麼HashMap初始長度採用16呢?我們可以對比長度是10的情況。
重複剛纔的運算步驟:
單獨看這個結果,表面上並沒有問題。我們再來嘗試一個新的HashCode 101110001110101110 1011:
我們再換一個HashCode 101110001110101110 1111試試:
對比結果可以看到,雖然HashCode的倒數第二位、第三位從0變成了1,但是運算的結果都是1001。也就是說,當HashMap長度爲10的時候,有些index結果的出現機率會更大,而有些index結果永遠不會出現(比如0111)。
這樣,顯然不符合Hash算法均勻分佈的原則。
反觀長度16或其他2的冪,Length-1的值是所有二進制全爲1,這種情況下,index的結果等同於HashCode後幾位的值。只要輸入的HashCode本身分佈均勻,Hash算法的結果就是均勻的。
2、高併發下的HashMap
(1) Hashmap在插入元素過多的時候需要進行Resize,Resize的條件是
HashMap.size >= Capacity * LoadFactor
其中,Capacity是HashMap的當前長度,HashMap的長度是2的冪。
LoadFactor是HashMap負載因子,默認值爲0.75f。
(2) HaspMap的Resize包含兩個步驟:
一是擴容。創建一個新的Entry空數組,長度是原數組的2倍。
二是ReHash,遍歷原Entry數組,把所有的Entry重新Hash到新數組。
需要注意的是,ReHash在併發的情況下可能形成鏈表環。