HashMap源碼(三) —— 成員變量解釋和構造方法細節分析

  1. HashMap數組容量,默認是16

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
    
  2. HashMap最大容量:230

    static final int MAXIMUM_CAPACITY = 1 << 30;
    
  3. 默認負載因子,也叫加載因子 0.75

    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    

    作用:影響着hashMap擴容的因素
    如:默認容量是16,保存的容量超過 16*0.5 =12 的時候就會擴容

  4. 鏈表轉紅黑樹臨界值(jdk8之後新增的)

    //當鏈表的值超過8則會轉紅黑樹
    static final int TREEIFY_THRESHOLD = 8;
    

    注:爲什麼這個節點臨界值會選擇8呢?
    是因爲根據統計學上的泊松分佈計算出節點數爲8的的概率是0.00000006,
    而鏈表結構轉換成紅黑樹要進行一系列的計算和拷貝且紅黑樹佔用的空間是鏈表結構的兩倍。
    所以主要是考慮到時間和空間之前的權衡,8的時候效率和性能上爲最佳。
    參考:https://blog.csdn.net/wj1314250/article/details/90438488

  5. 紅黑樹轉鏈表結構的臨界值

    //當桶(bucket)上的節點數小於6的時候,則會從紅黑樹轉換成鏈表結構
    static final int UNTREEIFY_THRESHOLD = 6;
    
  6. 桶中轉化爲紅黑書對應的數組長度最小的值限制

    static final int MIN_TREEIFY_CAPACITY = 64;
    

    注:當數組長度大於64並且節鏈表節點數大於8時纔會轉換成紅黑樹結構
    而當數組長度小於64鏈表長度大於8時,map會擴容。

  7. 存儲元素的數組

    // 在jdk8 之前 是 Entry<K,V>[] table
    transient Node<K,V>[] table;
    
  8. HashMap將數據轉換成set的另一種存儲形式,這個變量主要用於迭代功能。

    transient Set<Map.Entry<K,V>> entrySet;
    
  9. 存放的的節點個數 不是數組長度

    transient int size;
    
  10. HashMap的數據被修改的次數

    //這個變量用於迭代過程中的Fail-Fast機制,
    //其存在的意義在於保證發生了線程安全問題時,能及時的發現(操作前備份的count和當前modCount不相等)並拋出異常終止操作。
    transient int modCount;
    
  11. 加載因子,默認0.75

    // 計算公式 = size / capacity  即:存放個數/數組容量
    final float loadFactor;
    

    注:loadFactor太大,導致元素查找效率低,太小會導致數組利用率低,默認0.75是官方給出的一個比較好的臨界值
    當HashMap裏面容納的元素已經達到HashMap數組長度的75%時,表示HashMap太擠了,需要擴容,而擴容這個過程涉及到 rehash、複製數據等操作,非常消耗性能。,所以開發中儘量減少擴容的次數,可以通過創建HashMap集合對象時指定初始容量來儘量避免。也可以指定loadFactor,不建議修改。 可以通過該構造函數指定初始容量和加載因子,public HashMap(int initialCapacity, float loadFactor)
    爲什麼加載因子設置爲0.75,初始化臨界值是12
    因爲loadFactor越趨近於1,那麼 數組中存放的數據(entry)也就越多,也就越密,也就是會讓鏈表的長度增加,loadFactor越小,也就是趨近於0,數組中存放的數據(entry)也就越少,也就越稀疏。所以兼顧數組利用率和鏈表結構不要太長,經官方大量的測試後,0.75是最佳方案。

  12. 擴容臨界值

    //當實際大小(容量*負載因子)超過這個值時,會進行擴容
    int threshold;
    

    注:臨界值threshold的計算公式 capacity(數組長度默認16)*0.75.
    當size >= threshold 時,就會對數組resize(擴容),也就是說threshold是衡量數組是否擴容改的一個標準,擴容後的map是原來的兩倍

  13. 構造方法 public HashMap(Map<? extends K, ? extends V> m) 分析

    //構造方法,其它幾個構造相對簡單,不作說明
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
    //具體實現
    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();  //獲取參數集合長度
        if (s > 0) {
            if (table == null) {   // 判斷table是否初始化,未初始化,即第一次創建時table爲null
                float ft = ((float)s / loadFactor) + 1.0F;   //s/loadFactor  計算容量,值爲負數 , +1.0F 作用是向上取整,保證更大容量,減少擴容次數
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                if (t > threshold)             //初始的時候 邊界值是0
                   
                   //初始化臨界值
                   //對於這兒初始化的臨界值是一個2的n次冪值的原因,下面會做解釋
                   //tableSizeFor 方法解釋在之前文章中有介紹
                    threshold = tableSizeFor(t);  
            }
    		// 已初始化,並且m元素個數大於閾值,進行擴容處理
            else if (s > threshold)
                resize();
             //將m中的元素遍歷添加到hashmap中
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }
        }
    }
    

    對於 this.threshold = tableSizeFor(initialCapacity); 疑問解答
    tableSizeFor(initialCapacity) 判斷指定的初始化容量是否是2的n次冪,如果不是那麼會變爲比指定初始化容量大的最小的2的n次冪。這個之前文章有介紹到。
    但是注意,在tableSizeFor方法體內部將計算後的數據返回給調用這裏了,並且直接賦值給threshold邊界值了。
    有些人會覺得這裏是一個bug,應該這樣書寫:
    this.threshold = tableSizeFor(initialCapacity) * this.loadFactor;
    這樣才符合threshold的意思(當HashMap的size到達threshold這個閾值時會擴容)。
    但是,請注意,在jdk8以後的構造方法中,並沒有對table這個成員變量進行初始化,table的初始化被推遲到了put方法中,在put方法中會對threshold重新計算。
     
    float ft = ((float)s / loadFactor) + 1.0F;這一行代碼中爲什麼要加1.0F?
    s/loadFactor的結果是小數,加1.0F與(int)ft相當於是對小數做一個向上取整以儘可能的保證更大容量,更大的容量能夠減少resize的調用次數。所以 + 1.0F是爲了獲取更大的容量。
    例如:原來集合的元素個數是6個,那麼6/0.75是8,是2的n次冪,那麼新的數組大小就是8了。然後原來數組的數據就會存儲到長度是8的新的數組中了,這樣會導致在存儲元素的時候,容量不夠,還得繼續擴容,那麼性能降低了,而如果+1呢,數組長度直接變爲16了,這樣可以減少數組的擴容。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章