HashMap 也是比較常用的 Java 集合框架類,該類涉及到的知識比較多,包括陣列、連結串列、紅黑樹等等,還有一些高效巧妙的計算,並且這個類經過幾個版本的改進,不同版本之間是有些差異的,這裡都是基於 JDK8 原始碼。照常的原始碼翻譯,看看你能否回答下面的幾個問題?(一些地方真的很難翻譯,大家看看就好)
問題 1:HashMap 中的 initCapacity、size、threshold、loadFactor、bin 的理解?
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 內部是陣列+連結串列+紅黑樹實現的,為每個 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 方法的分析,整個過程比較複雜,流程圖如下:
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 時有死鏈問題、容易丟失資料,多執行緒中不要使用) |