使用 HashMap 存一萬條數據,構造時傳 10000 還會觸發擴容嗎?
問題
向 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。