HashMap特性、原理及算法實現的一些思考

1、HashMap 一些特性:

  • 存儲的是 <key, value> 形式的鍵值對;
  • 允許 key值 或 value值 爲 null;
  • HashMap 是非 synchronized;
  • HashMap 很快
  • 哈希表的主幹是數組,數組中的元素是鏈表,在 JDK8 中如果同一 hash 組成的鏈表元素大於等於 8 時,此數組元素將被調整成一顆紅黑樹。

 

2、HashMap 的工作原理:

        HashMap 是基於 hashing 的原理(不管後面進行什麼操作都是要先對鍵值進行 hash 處理),我們 put(key, value) 來存儲數據到 HashMap 中,使用 get(key) 從 HashMap 中獲取數據,使用 resize() 來擴容。

        我們在使用 put() 方法傳遞鍵值對對象時,首先對鍵值調用 hash() 方法,得到 hash值, 利用此 hash 值查找此次傳入鍵值對數據在數組中的存儲位置。

       我們使用 get() 方法從 HashMap 中取值,它同樣先是對鍵值調用 hash() 方法求得 hash 值,通過運算求得其在數組中的位置。

       默認的負載因子是 0.75,當 HashMap 的大小超過了 當前容量* 負載因子時, 它會進行 resize() 操作完成擴容,擴容後容量是之前的兩倍,同時將之前的對象重新放入到新的 HashMap 數組中。

 

3、引申出的一些問題:

(1)當使用 put() 方法時,兩個對象計算出的 hash 值 相同會發生什麼?

       當兩個對象計算出的 hash值 相同時,他們在數組中的位置就是一樣的,一般來說就會發生碰撞,但是 HashMap 中的數組中的每個元素都使用鏈表的方式存儲對象,這時會將新傳入的鍵值對存入到對應位置的鏈表中。

(2)如果兩個鍵計算出的 hash 值,你如何獲取值對象?

       首先通過對鍵值調用 hash() 方法獲得其對應的 hash 值,找到在數組中的位置,然後判斷鏈接第一個節點是否滿足,滿足則直接返回值對象;不滿足的話則繼續遍歷鏈表,直到找到鏈表中的鍵值對中的鍵值與傳入鍵值相等爲止,後返回對應的值對象。

(3)如果HashMap的大小超過了負載因子(load factor)定義的容量,怎麼辦?

       默認的負載因子是 0.75,當 HashMap 的大小超過了 當前容量* 負載因子時, 它會進行 resize() 操作完成擴容,擴容後容量是之前的兩倍,同時將之前的對象重新放入到新的 HashMap 數組中,這個過程叫作 rehashing,因爲它調用了 hash() 方法尋找了其新的數組位置。

 

4、擴展一下:

       JDK7 將新的元素插入到鏈表的頭部,因爲他們認爲新插入的元素被用到機會比較大,放在頭部便於查詢,避免了尾部遍歷。

       但是其在多線程的情況下會出現死循環,因爲在擴容的時候,採用頭部插入的方式可能會讓之前存儲在鏈表中的元素的次序會反過來,多線程的時候就會形成環形鏈表,出現死循環。

       JDK8 對這個對於新對象插入鏈表做了調整,採用尾部插入,有效的改進了這種情況。而且多線程的情況下一般不用 HashMap,而是要使用 ConcurrentHashMap。

 

5、思考:HashMap 初始長度,這樣設計的目的?

        HaspMap 的默認初始長度是16,並且每次擴展長度或者手動初始化時,長度必須是2 的次冪。之所以是 16,是爲了服務於從 Key 值映射到 index 的 hash 算法。

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

       n 是 HashMap 的容量,以 n = 16爲例,與操作保證了獲取 hash 的低 4 位,即爲此 key 值對應的數組位置 index。

       同時爲了實現一個儘量分佈均勻的 hash() 函數,利用的是 Key 值的 HashCode 來做某種運算。

 

      JDK8 中使用如下方式來實現。

(h = key.hashCode()) ^ (h >>> 16)

        hashCode 高16位保持不變,低 16 位與高 16 位進行異或,這樣可以保證數組比較小時候高低位都能參與到運算中, JDK7 有類似的四次擾動計算,JDK8 只有這麼一次,可能是從速度或者效率方面的考慮。

例:

  •         假設現在 hashCode 只有 8 位
  •         採用 (h = key.hashCode()) ^ (h >>> 4) 來實現 hash()
  •         計算在數組中的位置 index = tab[i = (n - 1) & hash] (n=16)
  •         如果此時有低四位相同,高四位不同的兩個 hashCode

        0100 1000 無符號右移 4 位: 0000 0100 計算得到 hash = 0100 1100 index = 12

        0010 1000 無符號右移 4 位: 0000 0010 計算得到 hash = 0010 1010 index = 10

        通過上述方法就可以獲得不同的 index 值,讓高低位都參與運算有利於對象在數組中均勻分佈,如果直接使用原始的 hashCode 值或者做取模運算則會等到兩個一樣的值,使得分佈不均勻。同理 hashCode 爲 32 位也是一樣的道理。

 

6、JDK7 與 JDK8 中 HashMap 的區別:

  • JDK7 之前的 HashMap 又叫散列鏈表:基於一個數組及多個鏈表的形式存儲,hash 值衝突的時候,就將對應節點放到鏈表中存儲。
  • JDK8 中,當同一個 hash (在 Table 上的元素)的鏈表節點數大於等於8時,將不在以單鏈表的形式存儲了,會被調整成一顆紅黑樹,這就是 JDK8 與 JDK7 中HashMap 實現的最大區別。
  • JDK8 中,對於同一 hash 組成的鏈表的元素插入是在鏈表尾部插入的,JDK7 及以前版本是在鏈表頭部插入的

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章