最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

一篇文章徹底拆解Java?HashMap擴容機制

 更新時間:2026年04月01日 10:34:38   作者:Anastasiozzzz  
在Java中HashMap是一個非常常用的數(shù)據(jù)結(jié)構(gòu),基于哈希表實現(xiàn),它通過鍵值對的形式存儲數(shù)據(jù),這篇文章主要介紹了Java HashMap擴容機制的相關(guān)資料,文中通過代碼介紹的非常詳細,需要的朋友可以參考下

今天來給大家再講解一下HashMap的擴容機制。一個小小的數(shù)據(jù)結(jié)構(gòu)里面隱藏著許多優(yōu)化與細節(jié)!

一、為什么需要擴容?

HashMap 的底層數(shù)據(jù)結(jié)構(gòu)是 數(shù)組 + 鏈表(JDK 1.8 引入了紅黑樹)。

數(shù)組的長度是固定的。隨著我們不斷執(zhí)行 put 操作,越來越多的鍵值對(Entry/Node)被放入容器。為了解決哈希沖突,鏈表會越來越長。

  • 問題:鏈表越長,查詢效率越低,時間復(fù)雜度從 O(1) 退化為 O(n)。

  • 解決:當元素數(shù)量達到一定閾值時,HashMap 會自動創(chuàng)建一個更大的數(shù)組,并將所有數(shù)據(jù)重新搬家到新數(shù)組中。這個過程就是 擴容(Resize)

核心參數(shù)

  • Capacity (容量):HashMap 中數(shù)組(bucket)的長度,默認 16。

  • LoadFactor (負載因子):衡量 HashMap 滿的程度,默認 0.75f。

  • Threshold (擴容閾值)capacity * loadFactor。當 size 超過這個值時,觸發(fā)擴容。

思考:為什么負載因子是 0.75? 這是一個空間與時間的折中(Trade-off)。

  • 如果是 1.0:空間利用率高,但哈希沖突概率大,查詢慢。

  • 如果是 0.5:哈希沖突少,查詢快,但浪費一半空間。

  • 0.75 是基于泊松分布統(tǒng)計學(xué)原理得出的黃金平衡點。

二、擴容的觸發(fā)時機

在 put 方法存入數(shù)據(jù)后,HashMap 會檢查當前元素個數(shù) size 是否大于 threshold。

// 偽代碼邏輯
if (++size > threshold)
    resize();

JDK 1.8 是先插入數(shù)據(jù),再判斷是否需要擴容;而 JDK 1.7 是先判斷是否需要擴容,再插入數(shù)據(jù)。

三、擴容流程

當我們的HashMap中的元素個數(shù)(size)達到負載因子*容量之后,會觸發(fā)擴容

此時會建一個新數(shù)組,數(shù)組容量為old數(shù)組的倆倍 :newCap = oldCap*2

擴容分三種情況:

  • 僅有一節(jié)點
  • 已形成鏈表
  • 已形成紅黑樹

接下來分情況討論

3.0 高低位拆分原理

JDK 8 利用容量為 2 的冪次方的特性,通過 (e.hash & oldCap) == 0 判斷元素在新數(shù)組中的位置:

  • 低位(lo)hash & oldCap == 0 → 新索引 = 原索引
  • 高位(hi)hash & oldCap != 0 → 新索引 = 原索引 + oldCap

為什么呢?

我們在進行索引計算的時候,是使用hash(k) & (cap - 1) 計算的,那么這里面其實有個規(guī)律

我們每次擴容都是容量*2,假設(shè)舊的容量為16,那么新容量為16 * 2 = 32

原本是通過hash(k) & (01111)進行計算索引的(01111是16-1的二進制),新數(shù)組的索引是通過hash(k) & (011111) 進行計算的,會發(fā)現(xiàn)其實是否遷移新的位置取決于第原本hash的

log(cap) + 1位,對于16就是4 + 1 = 5位,也就是說,如果我們的hash&cap == 0就放置原索引,如果hash&cap != 0就放到原索引+oldcap

3.1 僅有一節(jié)點

這是最簡單的情景,只要把這個節(jié)點遷移到新數(shù)組即可

if (e.next == null)
    newTab[e.hash & (newCap - 1)] = e;

假設(shè)原桶(索引 3)中有一條鏈表:A → B → C → D → null

3.2 已形成鏈表

遷移步驟:

  1. 初始化兩條空鏈表
    • 低位鏈表:loHead=null, loTail=null
    • 高位鏈表:hiHead=null, hiTail=null
  2. 逐節(jié)點判斷并拆分

    節(jié)點

    hash & 16 結(jié)果

    操作

    A

    ==0(低位)

    loHead=A, loTail=A → 低位鏈表:A → ?

    B

    !=0(高位)

    hiHead=B, hiTail=B → 高位鏈表:B → ?

    C

    ==0(低位)

    loTail.next=C, loTail=C → 低位鏈表:A → C → ?

    D

    !=0(高位)

    hiTail.next=D, hiTail=D → 高位鏈表:B → D → ?

  3. 斷尾與安置
    • loTail.next = null → 低位鏈表變?yōu)?A → C → null
    • hiTail.next = null → 高位鏈表變?yōu)?B → D → null
    • 低位鏈表放入 newTab[3]
    • 高位鏈表放入 newTab[19]

對于每個形成鏈表的桶都會重復(fù)這個步驟

也就是說會通過對每個桶的鏈表通過hash&oldCap的情況來分成倆個鏈表,然后再進行遷移

3.3 已形成紅黑樹

紅黑樹節(jié)點(TreeNode)同時維護兩種結(jié)構(gòu)

  • 紅黑樹結(jié)構(gòu):parent/left/right(用于查找)
  • 雙向鏈表:prev/next(用于遍歷和擴容)

擴容時只利用雙向鏈表進行拆分,不操作樹結(jié)構(gòu)。

階段 1:拆分為兩條 TreeNode 鏈表

假設(shè)原桶(索引 3)中有 9 個 TreeNode 節(jié)點,按雙向鏈表順序遍歷:

  1. 初始化:
    • 低位:loHead=null, loTail=null, lc=0
    • 高位:hiHead=null, hiTail=null, hc=0
  2. 逐節(jié)點拆分(與鏈表邏輯一致,但維護 prev 指針):

    節(jié)點

    hash & 16

    操作

    低位鏈表

    高位鏈表

    lc/hc

    N1

    ==0

    插入低位

    N1

    -

    lc=1

    N2

    !=0

    插入高位

    N1

    N2

    hc=1

    N3

    ==0

    插入低位

    N1→N3

    N2

    lc=2

    N4

    !=0

    插入高位

    N1→N3

    N2→N4

    hc=2

    ...

    ...

    ...

    ...

    ...

    ...

    N9

    ==0

    插入低位

    N1→N3→N5→N7→N9

    N2→N4→N6→N8

    lc=5, hc=4

    每次插入時:

    • 設(shè)置 e.prev = loTail(維護雙向鏈表)
    • loTail.next = e(連接到尾部)
    • loTail = e(更新尾指針)
    • e.next = null立即斷開原鏈表引用,防止內(nèi)存泄漏)

階段 2:計數(shù)完成后的決策

拆分結(jié)束后,根據(jù)每條鏈表的節(jié)點數(shù)做決策:

鏈表

節(jié)點數(shù)

決策邏輯

結(jié)果

低位

5

5 <= 6(退化閾值)

調(diào)用 untreeify() → 轉(zhuǎn)為普通 Node 鏈表

高位

4

4 <= 6

調(diào)用 untreeify() → 轉(zhuǎn)為普通 Node 鏈表

退化閾值 = 6UNTREEIFY_THRESHOLD
樹化閾值 = 8TREEIFY_THRESHOLD
6 < 8 的設(shè)計是為了防止"抖動":避免擴容拆分后頻繁在樹/鏈表間切換

階段 3:樹化條件(僅當節(jié)點數(shù) > 6 時觸發(fā))

若某條鏈表節(jié)點數(shù) > 6,還需滿足兩個條件才重建紅黑樹:

  1. 節(jié)點數(shù) > 6(已滿足)
  2. 數(shù)組總長度 ≥ 64MIN_TREEIFY_CAPACITY

滿足條件后執(zhí)行 treeify()

  • 按鏈表順序逐個插入到新紅黑樹中
  • 每次插入后調(diào)用 balanceInsertion() 進行平衡調(diào)整(旋轉(zhuǎn) + 變色)
  • 最后調(diào)用 moveRootToFront() 將樹根移到雙向鏈表頭部

若數(shù)組長度 < 64(如剛擴容到 32),即使節(jié)點數(shù) > 6 也不樹化,保持為 TreeNode 鏈表。原因:容量仍較小,下次擴容可能又拆分退化,避免無效樹化。

也就是拆分成倆個鏈表->判斷是否樹化->遷移至新桶

3.4 Talk is cheap Show me code

看看源碼加注釋吧

單節(jié)點、鏈表遷移

// HashMap.resize() 方法片段 - 鏈表遷移
for (int j = 0; j < oldCap; ++j) {          // 遍歷舊數(shù)組每個桶
    Node<K,V> e;
    if ((e = oldTab[j]) != null) {          // 桶非空
        oldTab[j] = null;                   // 清空原桶,便于GC
        if (e.next == null)                 // 單節(jié)點:直接遷移
            newTab[e.hash & (newCap - 1)] = e;
        else if (e instanceof TreeNode)     // 紅黑樹:調(diào)用split()(見下文)
            ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
        else {                              // ===== 鏈表遷移核心邏輯 =====
            // 初始化兩條新鏈表的頭尾指針
            // lo = low(低位),hi = high(高位)
            Node<K,V> loHead = null, loTail = null;  // 低位鏈表:新索引 = 原索引
            Node<K,V> hiHead = null, hiTail = null;  // 高位鏈表:新索引 = 原索引 + oldCap
            Node<K,V> next;
            // 遍歷原鏈表,逐節(jié)點拆分
            do {
                next = e.next;  // 1. 保存當前節(jié)點的next,防止斷鏈后無法繼續(xù)遍歷
                // 2. 高低位判斷:(hash & oldCap) == 0 ?
                //    oldCap是2的冪(如16=0b10000),此操作等價于判斷hash的第5位是否為0
                //    - 為0:新索引 = 原索引(低位)
                //    - 非0:新索引 = 原索引 + oldCap(高位)
                if ((e.hash & oldCap) == 0) {
                    // ===== 低位鏈表插入(尾插法)=====
                    if (loTail == null)       // 首次插入:設(shè)置頭節(jié)點
                        loHead = e;
                    else                      // 非首次:尾節(jié)點的next指向當前節(jié)點
                        loTail.next = e;
                    loTail = e;               // 更新尾指針為當前節(jié)點
                } else {
                    // ===== 高位鏈表插入(尾插法)=====
                    if (hiTail == null)
                        hiHead = e;
                    else
                        hiTail.next = e;
                    hiTail = e;
                }
            } while ((e = next) != null);    // 移動到下一個節(jié)點,繼續(xù)遍歷
            // 3. 斷尾操作:必須將兩條鏈表的尾節(jié)點next置為null!
            //    原因:遍歷時未斷開原鏈表引用,若不置null可能形成跨桶環(huán)形引用
            if (loTail != null) {
                loTail.next = null;          // 低位鏈表尾部斷開
                newTab[j] = loHead;          // 放入新數(shù)組的原索引位置
            }
            if (hiTail != null) {
                hiTail.next = null;          // 高位鏈表尾部斷開
                newTab[j + oldCap] = hiHead; // 放入新數(shù)組的原索引+oldCap位置
            }
        }
    }
}

紅黑樹遷移

/**
 * 紅黑樹拆分方法(定義在 TreeNode 內(nèi)部類中)
 * 
 * @param map   當前 HashMap 實例
 * @param tab   擴容后的新數(shù)組
 * @param index 原桶在舊數(shù)組中的索引
 * @param bit   舊容量 oldCap(2的冪,如16)
 */
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
    // 1. 獲取當前桶的樹根節(jié)點(同時也是雙向鏈表頭節(jié)點)
    TreeNode<K,V> b = this;
    // 2. 初始化兩條新鏈表的頭尾指針(注意:此時仍是TreeNode,非普通Node)
    TreeNode<K,V> loHead = null, loTail = null;  // 低位鏈表
    TreeNode<K,V> hiHead = null, hiTail = null;  // 高位鏈表
    // 3. 計數(shù)器:記錄高低位鏈表的節(jié)點數(shù)量,用于后續(xù)退化判斷
    int lc = 0, hc = 0;
    // ===== 階段1:按雙向鏈表順序遍歷拆分 =====
    // 注意:這里遍歷的是雙向鏈表(通過next指針),不是紅黑樹結(jié)構(gòu)!
    for (TreeNode<K,V> e = b, next; e != null; e = next) {
        // 保存當前節(jié)點的next指針(雙向鏈表的next,非樹結(jié)構(gòu)的right)
        next = (TreeNode<K,V>)e.next;
        // 【關(guān)鍵】斷開原鏈表引用!
        // 原因:防止殘留引用導(dǎo)致內(nèi)存泄漏,也為后續(xù)重建樹做準備
        e.next = null;
        // 高低位判斷(與鏈表遷移邏輯完全相同)
        if ((e.hash & bit) == 0) {
            // 低位鏈表插入(維護雙向鏈表的prev指針)
            // e.prev = loTail:設(shè)置當前節(jié)點的prev指向原尾節(jié)點
            // 若loTail為null(首次插入),則e.prev = null
            if ((e.prev = loTail) == null)
                loHead = e;          // 首次插入:設(shè)置頭節(jié)點
            else
                loTail.next = e;     // 非首次:原尾節(jié)點的next指向當前節(jié)點
            loTail = e;              // 更新尾指針
            ++lc;                    // 低位計數(shù)+1
        } else {
            // 高位鏈表插入(邏輯同上)
            if ((e.prev = hiTail) == null)
                hiHead = e;
            else
                hiTail.next = e;
            hiTail = e;
            ++hc;                    // 高位計數(shù)+1
        }
    }
    // ===== 階段2:低位鏈表處理 =====
    if (loHead != null) {
        // 退化判斷:節(jié)點數(shù) ≤ 6 時退化為普通鏈表
        // UNTREEIFY_THRESHOLD = 6(定義在HashMap類中)
        if (lc <= UNTREEIFY_THRESHOLD) {
            // untreeify():將TreeNode鏈表轉(zhuǎn)換為普通Node鏈表
            // 過程:遍歷每個TreeNode,創(chuàng)建對應(yīng)的Node對象,丟棄樹結(jié)構(gòu)指針
            tab[index] = loHead.untreeify(map);
        } else {
            // 節(jié)點數(shù) > 6:保留為TreeNode,準備重建紅黑樹
            tab[index] = loHead;
            // 優(yōu)化:僅當高位鏈表非空時才重建樹
            // 原因:若高位為空,說明所有節(jié)點都在低位,樹結(jié)構(gòu)未被破壞,無需重建
            if (hiHead != null) {
                // treeify():將TreeNode鏈表重建為紅黑樹
                // 過程:按鏈表順序逐個插入,每次插入后進行平衡調(diào)整
                ((TreeNode<K,V>)loHead).treeify(tab);
            }
        }
    }
    // ===== 階段3:高位鏈表處理(邏輯同低位)=====
    if (hiHead != null) {
        if (hc <= UNTREEIFY_THRESHOLD) {
            tab[index + bit] = hiHead.untreeify(map);
        } else {
            tab[index + bit] = hiHead;
            if (loHead != null) {
                ((TreeNode<K,V>)hiHead).treeify(tab);
            }
        }
    }
}

四、總結(jié)

會發(fā)現(xiàn)Java眾多數(shù)據(jù)結(jié)構(gòu)中的一個HashMap就已經(jīng)有很多優(yōu)化了,通過一次次版本的迭代,優(yōu)化成了現(xiàn)在的樣子!

到此這篇關(guān)于Java HashMap擴容機制的文章就介紹到這了,更多相關(guān)Java HashMap擴容機制內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • 詳解mybatis collection標簽一對多的使用

    詳解mybatis collection標簽一對多的使用

    這篇文章主要介紹了mybatis collection標簽一對多的使用,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2020-06-06
  • Springboot框架整合添加redis緩存功能

    Springboot框架整合添加redis緩存功能

    緩存就是一個存儲器,在技術(shù)選型中,常用?Redis?作為緩存數(shù)據(jù)庫。緩存主要是在獲取資源方便性能優(yōu)化的關(guān)鍵方面。Redis?是一個高性能的?key-value?數(shù)據(jù)庫,接下來通過本文給大家介紹Springboot框架整合添加redis緩存功能,感興趣的朋友一起看看吧
    2021-11-11
  • 給Java菜鳥的一些建議_關(guān)于Java知識點歸納(J2EE and Web 部分)

    給Java菜鳥的一些建議_關(guān)于Java知識點歸納(J2EE and Web 部分)

    下面小編就為大家?guī)硪黄oJava菜鳥的一些建議_關(guān)于Java知識點歸納(J2EE and Web 部分)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-05-05
  • IntelliJ IDEA 2020最新激活碼(親測有效,可激活至 2089 年)

    IntelliJ IDEA 2020最新激活碼(親測有效,可激活至 2089 年

    這篇文章主要介紹了IntelliJ IDEA 2021最新激活碼(親測有效,可激活至 2089 年),非常不錯,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2020-04-04
  • Java的NIO之并發(fā)環(huán)境下非阻塞IO技術(shù)詳解

    Java的NIO之并發(fā)環(huán)境下非阻塞IO技術(shù)詳解

    這篇文章主要介紹了Java的NIO之并發(fā)環(huán)境下非阻塞IO技術(shù)詳解,Java NIO(New IO)是Java平臺提供的一種用于高效處理I/O操作的API,它引入了一組新的類和概念,以提供更好的性能和可擴展性,需要的朋友可以參考下
    2023-09-09
  • 關(guān)于Spring總結(jié)(必看篇)

    關(guān)于Spring總結(jié)(必看篇)

    下面小編就為大家?guī)硪黄P(guān)于Spring總結(jié)(必看篇)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-08-08
  • java中volatile和synchronized的區(qū)別與聯(lián)系

    java中volatile和synchronized的區(qū)別與聯(lián)系

    這篇文章主要介紹了java中volatile和synchronized的區(qū)別與聯(lián)系的相關(guān)資料,希望通過本文能幫助到大家,讓大家理解這部分內(nèi)容,需要的朋友可以參考下
    2017-10-10
  • SpringBoot RESTful風格入門講解

    SpringBoot RESTful風格入門講解

    RESTful是一種web軟件風格,它不是標準也不是協(xié)議,它不一定要采用,只是一種風格,它倡導(dǎo)的是一個資源定位(url)及資源操作的風格,這篇文章主要介紹了SpringBoot使用RESTful接口
    2022-11-11
  • springboot設(shè)置tomcat post參數(shù)大小限制修改方式

    springboot設(shè)置tomcat post參數(shù)大小限制修改方式

    在SpringBoot項目中使用富文本框上傳大量圖片時,會遇到請求體大小限制的問題,可以通過修改Tomcat配置文件或在application.properties中設(shè)置maxPostSize參數(shù)來解決這個問題,修改后需要重啟服務(wù)器,熱部署可能需要額外重啟才能生效
    2025-11-11
  • 詳解Java的初始化與清理

    詳解Java的初始化與清理

    這篇文章主要介紹了Java的初始化與清理,文中示例代碼非常詳細,幫助大家更好的理解和學(xué)習(xí),感興趣的朋友可以了解下
    2020-07-07

最新評論

大宁县| 高碑店市| 潼关县| 杨浦区| 富民县| 且末县| 山阳县| 丹凤县| 中宁县| 郑州市| 壤塘县| 微博| 肥乡县| 睢宁县| 五大连池市| 响水县| 织金县| 信阳市| 通化县| 新宾| 陵川县| 洛扎县| 湖北省| 运城市| 仁布县| 金华市| 榆中县| 钦州市| 顺昌县| 如东县| 永善县| 五寨县| 灵宝市| 彝良县| 深州市| 柳江县| 开封县| 岑溪市| 宾川县| 桃园县| 洪江市|