【Java容器】HashMap從入門到熟悉

1. HashMap從入門到熟悉

1. hash碰撞的解決方案

在這裏插入圖片描述
HashMap就是使用哈希表來存儲的。哈希表爲解決衝突,可以採用開放地址法鏈地址法等來解決問題,Java中HashMap採用了鏈地址法。鏈地址法,簡單來說,就是數組加鏈表的結合。在每個數組元素上都有一個鏈表結構,當數據被Hash後,得到數組下標,把數據放在對應下標元素的鏈表上。

2. 紅黑樹優化方案

2.1 爲什麼是長度爲8的時候發生轉換

Because TreeNodes are about twice the size of regular nodes, we use them only when bins contain enough nodes to warrant use (see TREEIFY_THRESHOLD). And when they become too small (due to removal or resizing) they are converted back to plain bins. In usages with well-distributed user hashCodes, tree bins are rarely used. Ideally, under random hashCodes, the frequency of nodes in bins follows a Poisson distribution (http://en.wikipedia.org/wiki/Poisson_distribution) with a parameter of about 0.5 on average for the default resizing threshold of 0.75, although with a large variance because of resizing granularity. Ignoring variance, the expected occurrences of list size k are (exp(-0.5) * pow(0.5, k) / factorial(k)). The first values are:

0: 0.60653066
1: 0.30326533
2: 0.07581633
3: 0.01263606
4: 0.00157952
5: 0.00015795
6: 0.00001316
7: 0.00000094
8: 0.00000006
more: less than 1 in ten million

理想情況下,在隨機哈希代碼下,桶中的節點頻率遵循泊松分佈,文中給出了桶長度k的頻率表。

由頻率表可以看出,桶的長度超過8的概率非常非常小。所以作者應該是根據概率統計而選擇了8作爲閥值,由此可見,這個選擇是非常嚴謹和科學的。

2.2 既然存在鏈表轉換爲紅黑樹,那麼是否存在紅黑樹轉換爲鏈表

HashMap在jdk1.8之後引入了紅黑樹的概念,表示若桶中鏈表元素超過8時,會自動轉化成紅黑樹若桶中元素小於等於6時,樹結構還原成鏈表形式。

  • 紅黑樹的平均查找長度是log(n),長度爲8,查找長度爲log(8)=3,鏈表的平均查找長度爲n/2,當長度爲8時,平均查找長度爲8/2=4,這纔有轉換成樹的必要;鏈表長度如果是小於等於6,6/2=3,雖然速度也很快的,但是轉化爲樹結構和生成樹的時間並不會太短。
  • 還有選擇6和8的原因是:
    • 中間有個差值7可以防止鏈表和樹之間頻繁的轉換。假設一下,如果設計成鏈表個數超過8則鏈表轉換成樹結構,鏈表個數小於8則樹結構轉換成鏈表,如果一個HashMap不停的插入、刪除元素,鏈表個數在8左右徘徊,就會頻繁的發生樹轉鏈表、鏈表轉樹,效率會很低。

3. 擴容發生的時間,爲什麼擴容是2倍,擴容的過程

3.1 擴容發生的時間

大於等於閾值—即當前數組的長度乘以加載因子的值的時候,就要自動擴容。

  • 負載因子
    • 過小:容易發生reszie,消耗性能
    • 過大:容易發生hash碰撞,鏈表變長,紅黑樹變高

3.2 爲什麼hashmap底層數組要保證是2的n次方

   	//hash值的計算分爲兩步:
    //1. 異或運算
    static final int hash(Object key) {   //jdk1.8 & jdk1.7
         int h;
         // h = key.hashCode() 爲第一步 取hashCode值
         // h ^ (h >>> 16)  爲第二步 高位參與運算
         return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    //2. 和數組長度與運算,分佈到原有數組中
    hash = h&(n-1)

得到 hash 值之後,再與數組的長度-1(length-1)進行一次與運算,因爲如果數組的長度是 2 的倍數,那麼length-1 的二進制一定是 …00001111…這種形式,也就是前面一定都是 0,後面全是1,那麼再與 hash 值進行與運算的時候,結果一定是在原來數組大小的範圍內,比如默認數組大小16-1=15 的二進制爲: 00000000 00000000 00000000 00001111,某 key 的hash 值爲:11010010 00000001 10010000 00100100,那麼與上面做與運算的時候,值會對後面的四位進行運算,肯定會落在0~15 的範圍內,假如不是 2 的倍數,那麼 length-1 的二進制後面就不可能全是 1,做與運算的時候就會造成空間浪費。

3.3 擴容的具體過程

  • 開闢了新的數組空間
  • 元素的位置要麼是在原位置,要麼是在原位置再移動2次冪的位置。

我們在擴充HashMap的時候,不需要像JDK1.7的實現那樣重新計算hash,只需要看看原來的hash值新增的那個bit是1還是0就好了,是0的話索引沒變,是1的話索引變成“原索引+oldCap”,可以看看下圖爲16擴充爲32的resize示意圖:

在這裏插入圖片描述

4 既然存在擴容,是否存在縮容

沒有縮容機制,沒有看到與resize()對應方法。

5 HashMap和HashTable、HashSet、LinkedHashMap

在這裏插入圖片描述

  • Hashtable:Hashtable是遺留類,很多映射的常用功能與HashMap類似,不同的是它承自Dictionary類,並且是線程安全的,任一時間只有一個線程能寫Hashtable,併發性不如ConcurrentHashMap,因爲ConcurrentHashMap引入了分段鎖。Hashtable不建議在新代碼中使用,不需要線程安全的場合可以用HashMap替換,需要線程安全的場合可以ConcurrentHashMap替換。
  • LinkedHashMap:LinkedHashMap是HashMap的一個子類,保存了記錄的插入順序,在用Iterator遍歷LinkedHashMap時,先得到的記錄肯定是先插入的,也可以在構造時帶參數,按照訪問次序排序。
  • TreeMap:TreeMap實現SortedMap接口,能夠把它保存的記錄根據鍵排序,默認是按鍵值的升序排序,也可以指定排序的比較器,當用Iterator遍歷TreeMap時,得到的記錄是排過序的。如果使用排序的映射,建議使用TreeMap。在使用TreeMap時,key必須實現Comparable接口或者在構造TreeMap傳入自定義的Comparator,否則會在運行時拋出java.lang.ClassCastException類型的異常。

5.2 HashMap和HashSet的區別

在這裏插入圖片描述

6. Hashmap爲什麼是線程不安全的【死鎖分析】

  • 表面原因
    • Hashmap的方法沒有使用synchronized進行同步
  • 實際原因
    • 如果能找到併發環境下的問題,就能證明是不安全的
    • 併發環境下,hashmap進入擴容的時候容易造成Entry鏈成環,在查詢等操作的時候容易造成死循環

7. TreeMap和HashMap有什麼區別

使用Iterator迭代器遍歷的時候,HashMap的結果是沒有排序的,而TreeMap輸出的結果是排好序的。

參考資料

Ref1:https://tech.meituan.com/2016/06/24/java-hashmap.html

https://yikun.github.io/2015/04/01/Java-HashMap%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86%E5%8F%8A%E5%AE%9E%E7%8E%B0/

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