問題
向 HashMap 中存 10000 條資料,初始化時,構造方法傳值 10000,會觸發擴容嗎?
Map<String,String> map = new HashMap<>(10000);
分析
乍一看
肯定會觸發擴容呀,因為 HashMap 中有個負載因子預設為 0.75,就是說儲存的數量超過容量的 75% 就會觸發擴容,put 到後 25% 的資料時,肯定就會觸發擴容。但事實真是這樣嗎?原始碼中有我們想知道的一切,真相只有一個。
分析原始碼
HashMap 的初始化
在 HashMap 中,提供了一個指定初始容量的構造方法 HashMap(int initialCapacity),這個方法再通過 DEFAULT_LOAD_FACTOR 呼叫 HashMap 另一個構造方法,初始化 loadFactor 為 0.75。
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {
......
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
構造方法初始化了兩個成員變數 threshold 和 loadFactor,其中 threshold 就是用來儲存觸發 HashMap 擴容的閾值,也就是說,當 HashMap 儲存的資料量達到 threshold 時,就會觸發擴容。
從改造方法中可以看出,threshold 並沒有直接使用傳入的 initialCapacity 作為擴容閾值,而是通過 tableSizeFor 方法處理後再賦值給 threshold。
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
這啥呀?看不懂也沒關係,我來解釋。tableSizeFor 的作用就是尋找大於 cap 的 2 的整數次方,例如如果傳入 10,經過 tableSizeFor 處理後返回 16。至於為什麼這樣做,不在此處詳細討論。
迴歸正題
還有一個問題,一直在說 threshold 是觸發擴容的閾值,即 (initialCapacity * loadFactor),但是我們再構造方法中使用 tableSizeFor 初始化 threshold,並沒有用到 loadFactor,其實這一步被移交給 resize 方法實現了,因為我們並沒有在構造方法中初始化雜湊表陣列,擴容閾值 threshold = initialCapacity * loadFactor,這一步在 resize 中完成。
如果我們從外部傳遞進來 10000 初始化 initialCapacity ,實際上經過 tableSizeFor 方法處理之後,最終 threshold 就會變成 2 的 14 次冪 16384,再在 resize 方法中乘上負載因子 0.75,實際在不觸發擴容的前提下,可儲存的資料容量是 12288(16384 * 0.75),用來存放 10000 條資料,綽綽有餘,所以並不會觸發擴容。
initialCapacity
關於如何選擇 initialCapacity,我們看看阿里巴巴 Java 開發規範,規範要求在初始化 HashMap 時,必須指定 initialCapacity,因為這樣可以減少 resize 次數,提高程式效率。因為 threshold = initialCapacity * loadFactor,所以 initialCapacity = (需要儲存元素個數 / loadFactor) + 1。
示例
想要使用 HashMap 存放 10000 條資料,應該設定 initialCapacity = 10000 / 0.75 + 1 = 13334,然後雜湊表容量會被 tableSizeFor 方法調整到 16384(2^14),threshold = 16384 * 0.75 = 12288 足夠儲存 10000 條資料而不會觸發擴容。
總結
- HashMap 構造方法傳遞的 initialCapacity 實際表示雜湊表的容量,不代表擴容閾值。
- 構造方法傳遞的 initialCapacity,會被 tableSizeFor 方法調整為大於它的 2 的整數次方。
- 如果使用 initialCapacity 進行初始化,HashMap 是否擴容,由 threshold 決定,擴容閾值 threshold = initialCapacity * loadFactor。
- 在初始化 HashMap 時,必須指定 initialCapacity 來提升效率,initialCapacity = (需要儲存元素個數 / loadFactor(預設0.75)) + 1。