使用 HashMap 存一萬條資料,構造時傳 10000 還會觸發擴容嗎?

Acx7 發表於 2022-01-25
HashMap

問題

  向 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。

相關文章