HashMap 也是比較經常使用的 Java 集合框架類,該類涉及到的知識比較多,包括數組、鏈表、紅黑樹等等,還有一些高效巧妙的計算,而且這個類通過幾個版本的改進,不一樣版本之間是有些差別的,這裏都是基於 JDK8 源碼。照常的源碼翻譯,看看你可否回答下面的幾個問題?(一些地方真的很難翻譯,你們看看就好)java
問題 1:HashMap 中的 initCapacity、size、threshold、loadFactor、bin 的理解?git
HashMap 存放的是鍵值對,但並非簡單的一個蘿蔔一個坑。github
一、在 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 內部是數組+鏈表+紅黑樹實現的,爲每一個 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 方法的分析,整個過程比較複雜,流程圖以下:
一、判斷鍵值對數組 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 時有死鏈問題、容易丟失數據,多線程中不要使用) |