語言小知識-Java HashMap類 深度解析

Wizey發表於2018-12-13

HashMap 也是比較常用的 Java 集合框架類,該類涉及到的知識比較多,包括陣列、連結串列、紅黑樹等等,還有一些高效巧妙的計算,並且這個類經過幾個版本的改進,不同版本之間是有些差異的,這裡都是基於 JDK8 原始碼。照常的原始碼翻譯,看看你能否回答下面的幾個問題?(一些地方真的很難翻譯,大家看看就好)

HashMap 原始碼翻譯

問題 1:HashMap 中的 initCapacity、size、threshold、loadFactor、bin 的理解?

HashMap 存放的是鍵值對,但並不是簡單的一個蘿蔔一個坑。

HashMap 名詞介紹

1、在 HashMap 的有參建構函式中,我們指定 initCapacity,但會取大於或等於這個數的 2 的次冪作為 table 陣列的初始容量,使用 tableSizeFor(int) 方法,如 tableSizeFor(10) = 16(2 的 4 次冪),tableSizeFor(20) = 32(2 的 5 次冪),也就是說 table 陣列的長度總是 2 的次冪。

2、size 記錄 HashMap 中儲存的鍵值對的個數。

3、threshold 用來儲存當前容量下最大的可儲存的鍵值對個數,或者說是 HashMap 擴容的臨界值,當 size >= threshold 時,HashMap 就會擴容,threshold = capacity(table 陣列的長度) * loadFactor,但是當指定 initCapacity 還沒 put 鍵值對時,threshold 暫時等於 capacity 的值。

4、loadFactor 為負載因子,負載因子越小,陣列空間浪費就越大,鍵值對的分佈越均勻,查詢越快,反過來負載因子越大,陣列空間利用率越高,鍵值對的分佈越不均勻,查詢越慢,所以要根據實際情況,在時間和空間上做出選擇。

5、bin 在 HashMap 的註釋中多次出現,但這個詞並不好翻譯,table 陣列的每個位置存放的元素(可能不止一個)構成 bin,陣列的每個位置可以看作一個容器或者說是一個桶,容器中存放著一個或多個元素。

因為 HashMap 只開放了獲取 size 引數的方法,所以如果想檢視其他引數的值,一般方法是不行的,可以使用反射獲取上面幾個引數的值,寫程式碼驗證一下,我的測試程式碼

問題 2:HashMap 內部是怎麼存放資料的?

HashMap 內部結構

HashMap 內部是陣列+連結串列+紅黑樹實現的,為每個 Node 確定在 table 陣列中的位置,計算公式是 index = (n - 1) & hash(n 為 table 陣列的長度),這裡的 hash 是通過 key 的 hashCode 計算出來的,計算公式是 hash = key.hashCode ^ (key.hashCode>>>16)。Node 中儲存著鍵值對的 key 和 value 和計算出來的 hash 值,還儲存著下一個 Node 的引用 next(如果沒有下一個 Node,next = null),在一個陣列位置上會對應一個單向連結串列。當連結串列長度超過連結串列樹化(將連結串列轉為樹結構)的閾值 8 時,連結串列將轉換為紅黑樹,來提高查詢速度。

問題 3:HashMap 擴容的方法?

當 HashMap 中的 size >= threshold 時,HashMap 就要擴容。HashMap 同 ArrayList 一樣,內部都是動態增長的陣列,HashMap 擴容使用 resize() 方法,計算 table 陣列的新容量和 Node 在新陣列中的新位置,將舊陣列中的值複製到新陣列中,從而實現自動擴容。

1、當空的 HashMap 例項新增元素時,會以預設容量 16 為 table 陣列的長度擴容,此時 threshold = 16 * 0.75 = 12。

2、當不為空的 HashMap 例項新增新元素陣列容量不夠時,會以舊容量的2倍進行擴容,當然擴容也是大小限制的,擴容後的新容量要小於等於規定的最大容量,使用新容量建立新 table 陣列,然後就是陣列元素 Node 的複製了,計算 Node 位置的方法是 index = (n-1) & hash,這樣計算的好處是,Node 在新陣列中的位置要麼保持不變,要麼是原來位置加上舊陣列的容量值,在新陣列中的位置都是可以預期的(有規律的),並且連結串列上 Node 的順序也不會發生改變(JDK7 中 HashMap 的計算方法是會改變 Node 順序的)。

擴容陣列節點移動

問題 4:HashMap put 方法詳解?

put 方法內部呼叫的是 putVal() 方法,所以對 put 方法的分析也是對 putVal 方法的分析,整個過程比較複雜,流程圖如下:

put 方法流程圖

1、判斷鍵值對陣列 table 是否為空或為 null,如果是呼叫 resize() 方法進行擴容

2、根據鍵值 key 計算 hash 值得到要插入的陣列索引 index,如果 table[index]==null,說明這個位置還麼有節點,直接新建節點新增到這個位置,轉向 7,如果 table[index] 不為空,轉向 3;

3、 判斷 table[index] 的第一個節點是否和 key 一樣,如果相同直接覆蓋 value,否則轉向 4,這裡的相同指的是key.hashCode 以及 key 的 equals 方法;

4、 判斷 table[index] 是否為 treeNode 節點,也即是 table[index] 位置是否存放的是紅黑樹,如果是紅黑樹,則直接在樹中插入鍵值對,否則轉向 5;

5、 遍歷 table[index],判斷連結串列長度是否大於 8,大於 8 的話把連結串列轉換為紅黑樹,在紅黑樹中執行插入操作,否則進行連結串列的插入操作;

6、遍歷過程中若發現 key 已經存在直接覆蓋 value 即可;

7、 插入成功後,判斷實際存在的鍵值對數量 size 是否超多了最大容量 threshold,如果超過,進行擴容 resize。

問題 5:HashMap 陣列的長度為什麼是2的次冪?

主要原因是方便計算出 Node 在陣列中的位置 index,提高計算速度。理想情況下,HashMap 中的 table 陣列只存放一個 Node,也即是沒有雜湊碰撞,這樣存取效率都是最高的。但實際情況是,碰撞是很難避免的,我們要做的是儘可能的把資料均勻分佈在 table 陣列中,常規的做法是使用 hash % length = index 計算出 Node 在陣列中的位置,這個公式可以替換為 index = hash - (hash / length) * length,但這樣計算是比較複雜的,我們人類使用十進位制,而計算機使用的是二進位制,2 的次冪用二進位制表示是非常有規律的,如(16)10 = (10000)2,更巧妙的是當 length = 2 的次冪時,hash % length = hash & (length - 1),位運算在計算機中效率是很高的,這裡的 length - 1 也同樣很有規律,如(15)10 = (01111)2,任何一個 hash 值和 01111 做與的位運算,結果都是在 00000~01111(0~15) 這個範圍,而這也正好是陣列的 index。並且 HashMap 擴容時,table 陣列的長度是原來的兩倍,還是 2 的次冪,始終可以很快地計算 Node 在陣列中的位置 index。

問題 6:幾種 Map 集合類的對比?

Map 集合類 key value Super JDK 說明
Hashtable 不允許為 null 不允許為 null Dictionary 1.0 執行緒安全(過時)
ConcurrentMap 不允許為 null 不允許為 null AbstractMap 1.5 執行緒安全(JDK1.8 採用鎖分段和CAS,效能也很不錯)
TreeMap 不允許為 null 允許為 null AbstractMap 1.2 執行緒不安全(有序的)
HashMap 允許為 null 允許為 null AbstractMap 1.2 執行緒不安全(resize 時有死鏈問題、容易丟失資料,多執行緒中不要使用)

相關文章