Java HashMap從鏈表到紅黑樹的"進(jìn)化"過程詳解
在 Java 集合框架中,HashMap 的底層實(shí)現(xiàn)在 JDK 1.8 迎來了一次重大革新:引入了紅黑樹。這一設(shè)計(jì)并非為了酷炫,而是為了解決哈希碰撞導(dǎo)致的性能退化問題。本文將結(jié)合底層源碼,帶你徹底搞懂 HashMap 是在什么條件下、如何進(jìn)行樹化的。
一、 核心源碼常量定義
在 HashMap.java 中,有三個關(guān)鍵常量決定了樹化與退化的閾值:
/** * 1. 樹化閾值:當(dāng)桶中鏈表長度大于該值時,嘗試轉(zhuǎn)為紅黑樹 */ static final int TREEIFY_THRESHOLD = 8; /** * 2. 退化閾值:當(dāng)擴(kuò)容或刪除節(jié)點(diǎn)導(dǎo)致樹節(jié)點(diǎn)數(shù)小于該值時,轉(zhuǎn)回鏈表 */ static final int UNTREEIFY_THRESHOLD = 6; /** * 3. 最小樹化容量:只有當(dāng)數(shù)組總?cè)萘看笥谠撝禃r,才會真正進(jìn)行樹化 */ static final int MIN_TREEIFY_CAPACITY = 64;
二、 樹化的“雙重條件”深度邏輯
很多開發(fā)者只記得“鏈表長度 > 8”,但實(shí)際上源碼中存在一個隱藏的判定邏輯。
1. 觸發(fā)入口:putVal方法
當(dāng)我們在 put 一個元素時,如果發(fā)生碰撞且當(dāng)前是鏈表結(jié)構(gòu),會進(jìn)入以下邏輯:
// JDK 1.8 putVal 部分源碼
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null); // 插入新節(jié)點(diǎn)(尾插法)
if (binCount >= TREEIFY_THRESHOLD - 1) // 如果鏈表長度達(dá)到 8
treeifyBin(tab, hash); // 嘗試樹化
break;
}
// ... 忽略省略部分
}2. 核心判定:treeifyBin方法
進(jìn)入 treeifyBin 后,并不是直接轉(zhuǎn)紅黑樹,它會先檢查數(shù)組的長度:
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 【核心判定】
// 如果數(shù)組為空,或者數(shù)組長度 n < 64,則優(yōu)先選擇擴(kuò)容而不是樹化
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
// 只有數(shù)組長度 ≥ 64 且鏈表長度 > 8,才會執(zhí)行真正的樹化邏輯
// ... 將 Node 轉(zhuǎn)換為 TreeNode 的過程
}
}三、 深度思考:背后的數(shù)學(xué)與工程考量
1. 為什么是 8?—— 泊松分布
根據(jù) HashMap 源碼注釋,節(jié)點(diǎn)在哈希桶中的頻率遵循泊松分布。在負(fù)載因子為 0.75 的情況下,鏈表長度達(dá)到 8 的概率極低,約為 0.00000006。
設(shè)計(jì)用意:正常情況下,我們幾乎不會遇到樹化。紅黑樹是為了應(yīng)對那些哈希函數(shù)設(shè)計(jì)不佳,甚至遭受惡意哈希攻擊導(dǎo)致大量碰撞的情況。
2. 為什么退化閾值是 6 而不是 7?
這是為了留出緩沖區(qū)。如果退化閾值也是 8,那么當(dāng)一個桶的節(jié)點(diǎn)數(shù)在 7 和 8 之間反復(fù)變動時,會引起頻繁的“樹化 <-> 退化”轉(zhuǎn)換。這會導(dǎo)致大量的 TreeNode 與 Node 對象的創(chuàng)建與銷毀,嚴(yán)重影響性能。
3. 節(jié)點(diǎn)結(jié)構(gòu)的巨大變化
樹化不僅僅是邏輯變了,底層存儲的對象類型也發(fā)生了質(zhì)變:
- 鏈表節(jié)點(diǎn) (
Node):包含hash,key,value,next。 - 樹節(jié)點(diǎn) (
TreeNode):繼承自LinkedHashMap.Entry,除了基本屬性,還增加了parent,left,right,prev,red(紅黑屬性)。
空間代價:
TreeNode占用的內(nèi)存空間大約是普通Node的 2 倍。
四、 總結(jié):HashMap 的進(jìn)化準(zhǔn)則
鏈表轉(zhuǎn)紅黑樹:當(dāng)前桶鏈表長度

且數(shù)組總?cè)萘?
。
紅黑樹轉(zhuǎn)鏈表:在擴(kuò)容或刪除元素時,若樹中節(jié)點(diǎn)數(shù)
。
- 核心哲學(xué):
- 容量小、碰撞多:通過
resize擴(kuò)容來平攤碰撞。 - 容量大、碰撞多:通過
treeify提升查詢效率(從 O(n) 降至 O(log n))。
- 容量小、碰撞多:通過
?? 面試貼士
在面試中,如果面試官問:“HashMap 什么時候樹化?”,完整的回答應(yīng)該是:
“當(dāng)鏈表長度超過 8 時,HashMap 會調(diào)用
treeifyBin方法。但該方法內(nèi)部會先判斷數(shù)組容量,如果容量小于 64,會優(yōu)先擴(kuò)容;只有容量大于等于 64 且鏈表長度達(dá)到 8,才會正式轉(zhuǎn)換為紅黑樹。”
到此這篇關(guān)于深入淺出 Java HashMap:從鏈表到紅黑樹的“進(jìn)化”之路的文章就介紹到這了,更多相關(guān)Java HashMap鏈表到紅黑樹內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot?docker項(xiàng)目部署實(shí)戰(zhàn)
本文主要介紹了SpringBoot?docker項(xiàng)目部署實(shí)戰(zhàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-08-08
mybatis?返回Map類型key默認(rèn)為大寫問題
這篇文章主要介紹了mybatis?返回Map類型key默認(rèn)為大寫問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11
springboot的類加載器(org.springframework.boot.loader)過程詳解
這篇文章主要介紹了springboot的類加載器(org.springframework.boot.loader),本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-11-11
maven?scope?provided和runtime的例子說明
這篇文章主要介紹了maven?scope?provided和runtime的例子說明,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-12-12
查找native方法的本地實(shí)現(xiàn)函數(shù)native_function詳解
JDK開放給用戶的源碼中隨處可見Native方法,被Native關(guān)鍵字聲明的方法說明該方法不是以Java語言實(shí)現(xiàn)的,而是以本地語言實(shí)現(xiàn)的,Java可以直接拿來用。這里介紹下查找native方法的本地實(shí)現(xiàn)函數(shù)native_function,感興趣的朋友跟隨小編一起看看吧2021-12-12
使用Get方式提交數(shù)據(jù)到Tomcat服務(wù)器的方法
這篇文章將介紹向服務(wù)器發(fā)送數(shù)據(jù),并且服務(wù)器將數(shù)據(jù)的處理結(jié)果返回給客戶端,本文給大家介紹使用Get方式向服務(wù)器發(fā)送數(shù)據(jù),感興趣的朋友一起學(xué)習(xí)吧2016-04-04
SpringBoot中實(shí)現(xiàn)@Scheduled動態(tài)定時任務(wù)
SpringBoot中的@Scheduled注解為定時任務(wù)提供了一種很簡單的實(shí)現(xiàn),本文主要介紹了SpringBoot中實(shí)現(xiàn)@Scheduled動態(tài)定時任務(wù),具有一定的參考價值,感興趣的可以了解一下2024-01-01

