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

HashMap 也是比較經常使用的 Java 集合框架類,該類涉及到的知識比較多,包括數組、鏈表、紅黑樹等等,還有一些高效巧妙的計算,而且這個類通過幾個版本的改進,不一樣版本之間是有些差別的,這裏都是基於 JDK8 源碼。照常的源碼翻譯,看看你可否回答下面的幾個問題?(一些地方真的很難翻譯,你們看看就好)java

HashMap 源碼翻譯

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

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

HashMap 名詞介紹

一、在 HashMap 的有參構造函數中,咱們指定 initCapacity,但會取大於或等於這個數的 2 的次冪做爲 table 數組的初始容量,使用 tableSizeFor(int) 方法,如 tableSizeFor(10) = 16(2 的 4 次冪),tableSizeFor(20) = 32(2 的 5 次冪),也就是說 table 數組的長度老是 2 的次冪。數組

二、size 記錄 HashMap 中保存的鍵值對的個數。安全

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

四、loadFactor 爲負載因子,負載因子越小,數組空間浪費就越大,鍵值對的分佈越均勻,查找越快,反過來負載因子越大,數組空間利用率越高,鍵值對的分佈越不均勻,查找越慢,因此要根據實際狀況,在時間和空間上作出選擇。框架

五、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 在新數組中的新位置,將舊數組中的值複製到新數組中,從而實現自動擴容。

一、當空的 HashMap 實例添加元素時,會以默認容量 16 爲 table 數組的長度擴容,此時 threshold = 16 * 0.75 = 12。

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

擴容數組節點移動

問題 4:HashMap put 方法詳解?

put 方法內部調用的是 putVal() 方法,因此對 put 方法的分析也是對 putVal 方法的分析,整個過程比較複雜,流程圖以下:

put 方法流程圖

一、判斷鍵值對數組 table 是否爲空或爲 null,若是是調用 resize() 方法進行擴容

二、根據鍵值 key 計算 hash 值獲得要插入的數組索引 index,若是 table[index]==null,說明這個位置還麼有節點,直接新建節點添加到這個位置,轉向 7,若是 table[index] 不爲空,轉向 3;

三、 判斷 table[index] 的第一個節點是否和 key 同樣,若是相同直接覆蓋 value,不然轉向 4,這裏的相同指的是key.hashCode 以及 key 的 equals 方法;

四、 判斷 table[index] 是否爲 treeNode 節點,也便是 table[index] 位置是否存放的是紅黑樹,若是是紅黑樹,則直接在樹中插入鍵值對,不然轉向 5;

五、 遍歷 table[index],判斷鏈表長度是否大於 8,大於 8 的話把鏈表轉換爲紅黑樹,在紅黑樹中執行插入操做,不然進行鏈表的插入操做;

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

七、 插入成功後,判斷實際存在的鍵值對數量 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 時有死鏈問題、容易丟失數據,多線程中不要使用)
相關文章
相關標籤/搜索