Java中HashMap 初始化時容量(參數)如何設置合適?


問題引入

注:本文代碼源自java 9

  • 阿里的插件對於初始化HashMap時,調用無參構造方法,提示如下:
  • 那么問題來了,如果已知需要向 map 中 put n次,那么需要設定初始容量為多少?
  • 單純的我今天上午還認為是合理的容量是 n + 1 即可,直到看了源碼;
  • 應注意,map.size 獲取的是當前map中鍵值對的個數,而不是容量。

當初始化的時候,沒有指定容量,情況如何?

  • 直接調用如下構造函數(無參)
    /**
     * Constructs an empty {@code HashMap} with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
  • 首先應該清楚,loadFactor 是 裝載因子(通俗來講,若值為0.5f,那么當map元素達到 容量*0.5f的時候,就應該做擴容等操作)
  • HashMap對象實際上只有 threshold(臨界值,閾值) 屬性,每次put()后,需要比較的是閾值與size(map中已有鍵值對的個數)
  • 至於容量屬性,實際上各個方法中用的是 transient Node<K,V>[] table; 中 table 的長度
  • Hashmap結構可參考 http://www.importnew.com/20386.html
  • 本構造方法上說明:構造了一個空的HashMap,默認容量為16,裝載因子為0.75。
  • 然而可以清楚地看到,此構造方法只是將常量 DEFAULT_LOAD_FACTOR賦值給裝載因子,但沒有設定容量。
  • 當調用put()時,如下方式設定了初始容量。
  • 調用 putVal(hash(key), key, value, false, true)
public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
  • 在putVal()中,進入第一個if,調用resize()
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
   //省略無關代碼
}
  • 在下方代碼中,oldCap 與 oldThr 初始都為0,故直接進入else{}
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) 
       //省略無關代碼
    else if (oldThr > 0) // initial capacity was placed in threshold
        //省略無關代碼
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;//設定 newCap  = 16
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//設定 newThr  = 16 * 0.75 = 12
    }
        threshold = newThr;//將對象的 threshold(臨界值,閾值) 屬性設置為 newThr 
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = //將對象的 table 屬性設置為剛實例化的 newTab 
        //省略無關代碼
        return newTab;//並返回
    
  • 綜上,默認無參的構造方法設定裝載因子為0.75f,初始容量為16在調用put()時,設置為16。

當我們指定容量為n的時候,情況如何?

  • 即: Map<String,String> map = new HashMap<>(n);

  • 當指定容量時,調用如下構造方法:

/**
     * Constructs an empty {@code HashMap} with the specified initial
     * capacity and the default load factor (0.75).
     *
     * @param  initialCapacity the initial capacity.
     * @throws IllegalArgumentException if the initial capacity is negative.
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
  • 此方法將自定義容量作為參數1,將默認常量0.75f作為參數2,調用如下構造方法。
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}
  • 此方法先做了對於容量與裝載因子值的合理性做了判斷與處理;
  • 設定裝載因子值為0.75f,設置threshold 屬性為 tableSizeFor()參數為n時的返回值。
  • 對於tableSizeFor(initialCapacity),如下:
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;
}
  • 也就是傳入 n 的時候,返回了 第一個 比 n 大的 2的冪(2.4.8.16.32.64...)的數,例 n為 10,返回16;n為17 返回 32.
  • 具體實現原理可參考:http://blog.csdn.net/fan2012huan/article/details/51097331
  • 同上,對於在put方法中,對象的table屬性仍然為null,還是需要resize();
  • 本次調用與上次不同在於,threshold屬性 已經被賦值,假設值為 m(m>0);
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;//oldCap 值 為 0
    int oldThr = threshold;//oldThr 值 為 m,m > 0
    int newCap, newThr = 0;
    if (oldCap > 0) {
        //省略無關代碼
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;//newCap 被賦值為 m
     //省略無關代碼
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;//ft為m*0.75
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
        //在newCap 與ft 小於最大容量時(不滿足時不討論),執行  newThr  = ft
    }
    threshold = newThr;//threshold 被再次賦值為 ft--->m*0.75
    @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;//容量賦值為 m
    //省略無關代碼
    return newTab;
}
  • 綜上:當我們構造新的HashMap的時,傳入容量為n(假設為9),構造方法根據n得到閾值m(16)
    當調用put()時,直接將閾值設置為m*0.75(12) ,將容量設置為 m(16)
  • 也就是說,當n不是2的冪的時候,會根據n找到合適的數 m,並認為m是新的容量,閾值是m*0.75!

說了那么多,初始化的時候,到底怎么設置參數n

  • 如下是put方法的最后一步,需要進行的如下方式的驗證:
if (++size > threshold)
            resize();
  • 為了防止調用resize(),需要保證元素數量小於閾值(不可等於!)。
  • 綜上,需要根據調用put(key,value)的次數count(准確的說是key不重復時),找到 第一個 比 n 大的 2的冪的數m
    判斷裝載因子loadFactor(默認0.75)*m是否仍然不小於count,若滿足則設置為count,否則設置為2*m或者m+1
  • 例1,當我們需要在map里面裝 100個元素時,已知128是第一個大於100並且是2的冪的數,但128*0.75 = 96 還是小於100,
    那么顯然129-256(閉區間)作為初始化的參數更合適。
  • 例2,當我們需要在map里面裝 80個元素時,已知128是第一個大於80並且是2的冪的數,且128*0.75 = 96 還是大於80,
    那么顯然64-128(左開右閉)作為初始化的參數更合適。
  • 例3,當我們需要在map里面裝 12個元素時,應選用16為初始容量!

參考文章:http://blog.csdn.net/fan2012huan/article/details/51087722
http://yikun.github.io/2015/04/01/Java-HashMap工作原理及實現/
https://www.cnblogs.com/coderxuyang/p/3718856.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM