HashMap的擴容機制
閱讀此文章前最好看一下介紹HashMap的實現原理:
葉文潔:HashMap的實現原理
為了方便說明,這里明確幾個名詞:
- capacity 即容量,默認16。
- loadFactor 加載因子,默認是0.75
- threshold 閾值。閾值=容量*加載因子。默認12。當元素數量超過閾值時便會觸發擴容。
什么時候觸發擴容?
一般情況下,當元素數量超過閾值時便會觸發擴容。每次擴容的容量都是之前容量的2倍。
HashMap的容量是有上限的,必須小於1<<30,即1073741824。如果容量超出了這個數,則不再增長,且閾值會被設置為Integer.MAX_VALUE(
,即永遠不會超出閾值了)。
JDK7中的擴容機制
JDK7的擴容機制相對簡單,有以下特性:
- 空參數的構造函數:以默認容量、默認負載因子、默認閾值初始化數組。內部數組是空數組。
- 有參構造函數:根據參數確定容量、負載因子、閾值等。
- 第一次put時會初始化數組,其容量變為不小於指定容量的2的冪數。然后根據負載因子確定閾值。
- 如果不是第一次擴容,則
,
。
JDK8的擴容機制
JDK8的擴容做了許多調整。
HashMap的容量變化通常存在以下幾種情況:
- 空參數的構造函數:實例化的HashMap默認內部數組是null,即沒有實例化。第一次調用put方法時,則會開始第一次初始化擴容,長度為16。
- 有參構造函數:用於指定容量。會根據指定的正整數找到不小於指定容量的2的冪數,將這個數設置賦值給閾值(threshold)。第一次調用put方法時,會將閾值賦值給容量,然后讓
。(因此並不是我們手動指定了容量就一定不會觸發擴容,超過閾值后一樣會擴容!!) - 如果不是第一次擴容,則容量變為原來的2倍,閾值也變為原來的2倍。(容量和閾值都變為原來的2倍時,負載因子還是不變)
此外還有幾個細節需要注意:
- 首次put時,先會觸發擴容(算是初始化),然后存入數據,然后判斷是否需要擴容;
- 不是首次put,則不再初始化,直接存入數據,然后判斷是否需要擴容;
擴容時容量的計算方法說完了,下面說一說元素的遷移。
JDK7的元素遷移
JDK7中,HashMap的內部數據保存的都是鏈表。因此邏輯相對簡單:在准備好新的數組后,map會遍歷數組的每個“桶”,然后遍歷桶中的每個Entity,重新計算其hash值(也有可能不計算),找到新數組中的對應位置,以頭插法插入新的鏈表。
這里有幾個注意點:
- 是否要重新計算hash值的條件這里不深入討論,讀者可自行查閱源碼。
- 因為是頭插法,因此新舊鏈表的元素位置會發生轉置現象。
- 元素遷移的過程中在多線程情境下有可能會觸發死循環(無限進行鏈表反轉)。
JDK8的元素遷移
JDK8則因為巧妙的設計,性能有了大大的提升:由於數組的容量是以2的冪次方擴容的,那么一個Entity在擴容時,新的位置要么在原位置,要么在原長度+原位置的位置。原因如下圖:
數組長度變為原來的2倍,表現在二進制上就是多了一個高位參與數組下標確定。此時,一個元素通過hash轉換坐標的方法計算后,恰好出現一個現象:最高位是0則坐標不變,最高位是1則坐標變為“10000+原坐標”,即“原長度+原坐標”。如下圖:
(圖片來源於文末的參考鏈接)
因此,在擴容時,不需要重新計算元素的hash了,只需要判斷最高位是1還是0就好了。
JDK8的HashMap還有以下細節:
- JDK8在遷移元素時是正序的,不會出現鏈表轉置的發生。
- 如果某個桶內的元素超過8個,則會將鏈表轉化成紅黑樹,加快數據查詢效率。

