當前位置: 妍妍網 > 碼農

為什麽阿裏巴巴修正了 HashMap 關於 1024 個元素擴容的次數?

2024-03-19碼農

點選關註公眾號,Java幹貨 及時送達 👇

引言

最近在翻看【阿裏巴巴開發手冊-嵩山版】時,發現其修正了關於HashMap關於1024個元素擴容的次數 在先前的版本泰山版我們可以看到以下描述:

而嵩山版則可以看到:

同時我們也可在嵩山版的版本歷史中看到對以上變化的描述:

並且我在官網文件中也同樣發現了采訪視訊對這一變化,主持人對孤盡老師提出了以下疑問:

主持人:

有同學問,嵩山版修正了HashMap關於1024個元素擴容的次數?那麽這個泰山版中是七次擴容,我感覺是正確的,為什麽現在進行了修改?

孤盡老師:

大家可以看到嵩山版描述都改了,就沒有講「擴容」,講叫「resize」的次數。因為resize的這個次數的話,我們在調resize的時候,就put,如果你new了一個HashMap它並不會給你分配空間,第一次put的時候,它才會給你分配空間。所以就是說我們在第一次put的時候,也會調resize。但是很多同學就會爭議,說這個算不算擴容?那其實就是說我們在這個點上,其實為了,因為電腦我們大家都是搞電腦的,都希望用沒有「二義性」的語言來表示。所以在嵩山版本裏面進行了修改,然後我們改成了resize的方式來說明這到底是它的容量是如何變化的。因為擴容這個詞的話,大家的理解是不一樣的。這也是我們有一次大概在業界我們有一個打卡就是一個打卡測試題,這個題裏面我們有一個選項。當時選的同學是兩種答案,但是這兩種答案都是有自己的道理。所以我們也是借鑒了這個看法,然後在這個嵩山版裏面也寫的更加明確了。就是叫resize的次數,不叫擴容的次數。

以下是文件下載連結和采訪視訊地址。

  • https://developer.aliyun.com/topic/java20

  • 我們可以從孤盡老師的回答中提煉出主要發生的變化是:

  • 擴容次數修改為resize次數。主要的問題是每個人對「擴容」這個定義存在分歧。

  • 擴容的主要分歧是在:沒有給HashMap進行初始化的時候,第一次put的時候才會分配空間,也會呼叫resize。那這操作個算不算擴容?即初始化容量這個操作算不算擴容?

  • 所以擴容次數修改為resize次數。

  • 那麽我們先來看一下第一次put的時候呼叫resize這個操作是什麽?

    第一次put呼叫resize()

    前面孤盡老師的回答中也強調了這個put()方法呼叫resize()方法是存在一個條件的:

    如果你new了一個HashMap它並不會給你分配空間。同時文件中也寫了由於沒有設定容量初始大小。

    除了這個條件其實還有一個額外的條件,就是這是在JDK1.8之後HashMap才會有的操作。

    要理解這句話我們首先要看下HashMap的源碼,看下它的構造器方法。

    存在四個構造方法

    //參數:初始容量,負載因子
    publicHashMap(int initialCapacity, float loadFactor){ }
    //參數:初始容量。呼叫上一個建構函式,預設的負載因子(0.75)
    publicHashMap(int initialCapacity){ this(initialCapacity, DEFAULT_LOAD_FACTOR); }
    //參數:無參構造器。建立的HashMap具有預設的負載因子(0.75)
    publicHashMap(){ this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }
    //參數:使用與指定Map相同的對映構造一個新的HashMap。建立的HashMap具有預設的負載因子(0.75)和足夠在指定Map中保存對映的初始容量。
    publicHashMap(Map<? extends K, ? extends V> m){
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
    }

    也就是說當我們使用一個無參構造器建立HashMap的時候

    Map<String,String> map = new HashMap<>();

    呼叫的就是上述構造方法的第三個方法,只會設定預設的負載因子為0.75。就沒有其他操作了。。

    而當我們對這個HashMap進行put()操作的時候

    Map<String,String> map = new HashMap<>();
    map.put("first","firstValue");

    我們看一下源碼,進行了什麽操作?

    public V put(K key, V value){
    return putVal(hash(key), key, value, falsetrue);
    }
    /**
    * Implements Map.put and related methods.
    *
    @param hash hash for key
    @param key the key
    @param value the value to put
    @param onlyIfAbsent if true, don't change existing value
    @param evict if false, the table is in creation mode.
    @return previous value, or null if none
    */

    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;
    if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
    else {
    Node<K,V> e; K k;
    if (p.hash == hash &&
    ((k = p.key) == key || (key != null && key.equals(k))))
    e = p;
    elseif (p instanceof TreeNode)
    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    else {
    for (int binCount = 0; ; ++binCount) {
    if ((e = p.next) == null) {
    p.next = newNode(hash, key, value, null);
    if (binCount >= TREEIFY_THRESHOLD - 1// -1 for 1st
    treeifyBin(tab, hash);
    break;
    }
    if (e.hash == hash &&
    ((k = e.key) == key || (key != null && key.equals(k))))
    break;
    p = e;
    }
    }
    if (e != null) { // existing mapping for key
    V oldValue = e.value;
    if (!onlyIfAbsent || oldValue == null)
    e.value = value;
    afterNodeAccess(e);
    return oldValue;
    }
    }
    ++modCount;
    if (++size > threshold)
    resize();
    afterNodeInsertion(evict);
    returnnull;
    }

    其實呼叫的是內部的putVal()方法。同時我們也可看見我們前面的構造器方法中除了設定了負載因子,就沒有其他操作了,所以是符合 if ((tab = table) == null || (n = tab.length) == 0) ,大家也可debug斷點打在這進行驗證。所以就會執行下面的程式碼 n = (tab = resize()).length; 呼叫了一次resize(),同時對n進行了賦值操作。

    那麽我們透過源碼的確發現了,在JDK1.8中由於沒有設定HashMap容量初始大小,在第一次進行put()操作的時候,HashMap會呼叫一次resize()方法初始化容量為16。(resize的源碼也不是本篇文章的重點,所以我這邊會直接提供結論,並且再次強調一下JDK1.7是會初始化容量為16的,而不是在是第一次進行put()操作時初始化容量。

    呼叫resize()的次數

    在嵩山版中修訂了HashMap關於1024個元素擴容的次數修改為:resize()方法總共會呼叫 8 次。

    那麽我們來看一下到底是哪8次?

    再次重申一下本篇文章主要是講嵩山版對這個擴容次數的修改,涉及相關的源碼已在Java不得不知道的八股文之哈希表 中寫過,為了避免重復啰嗦,有水文之嫌,所以本文不會再次分析會直接給出對應的結論為已知條件進行分析。如想知道更多細節和版本差異,請移步之前的文章:

    https://juejin.cn/post/7168631540518223903

    以下是我們可以從源碼中得出的結論:

  • HashMap沒有初始化容量時預設負載因子為0.75,在第一次put()時會呼叫resize()進行初始化,容量為16。(JDK1.8)

  • HashMap中的閾值threshold為 capacity * load factor 容量*負載因子 。例如容量為16,那麽閾值threshold就為 16*0.75=12

  • 當HashMap進行put()的時候,元素個數大於等於當前閾值的時候 size>=threshold ,會進行resize()操作。容量和閾值都會乘以2。例如初始容量為16,那麽當我們put第12個元素的時候,就會進行resize()操作。

  • 基於以上已知的結論,那麽我們就能很容易的知道,在JDK1.8中沒有給HashMap初始化容量時,儲存1024個元素呼叫resize()的次數和時機了。(預設閾值為0.75)

    所以在第7次進行resize()後,HashMap的capacity為1024了,但是當我們存放第768個元素時, size>=threshold 就會進行第8次擴容,此時才能真正的存放下1024個元素。

    所以在JDK1.8中沒有給HashMap初始化容量時,儲存1024個元素,會呼叫resize()8次。

    其實resize()操作也是十分消耗效能的,我們儲存1024個元素的時候沒有設定初始值就會呼叫8次。如果我們給其設定了2048容量,那麽我們可以避免了。所以阿裏巴巴同時建議我們給HashMap初始化時給定容量。

    總結

    此番修正主要是每個人對「擴容」定義存在了分歧,在JDK1.8中如果沒有給HashMap設定初始容量,那麽在第一次put()操作的時候會進行resize()。而有的人認為這算一次擴容,有的人認為這不是一次擴容,這只是HashMap容量的初始化。

    所以儲存1024的元素時:

  • 前者的人認為擴容次數為8次。

  • 後者的人認為擴容次數為7次。

  • 孤盡老師說對此分歧,希望用沒有「二義性」的語言來表示,所以「擴容次數」修正為「resize次數」。

    來源|juejin.cn/post/7302724955699789863


    END


    看完本文有收獲?請轉發分享給更多人

    關註「Java編程鴨」,提升Java技能

    關註Java編程鴨微信公眾號,後台回復:碼農大禮包可以獲取最新整理的技術資料一份。涵蓋Java 框架學習、架構師學習等!

    文章有幫助的話,在看,轉發吧。

    謝謝支持喲 (*^__^*)