iT邦幫忙

2026 iThome 鐵人賽

0
Build on Google AI

把Google AI當夥伴打造個人高效Coding工作流系列 第 13 篇

第十三天HashMap雜湊演算法、容量2的N次方原理與JDK 8紅黑樹轉換機制

  • 分享至 

  • xImage
  •  

一、 HashMap 底層物理結構演進
JDK 7:陣列 + 單向鏈結串列(Array + Linked List)
發生雜湊碰撞時採用頭插法(Head Insertion)。
在多執行緒併發擴容情境下,指標容易形成環狀結構,導致 CPU 100% 的死迴圈(Infinite Loop)崩潰。
JDK 8+:陣列 + 單向鏈結串列 + 紅黑樹(Array + Linked List + Red-Black Tree)
解決鏈結過長時查詢時間複雜度由 O(1) 退化至 O(N) 的問題。
插入方式改採尾插法(Tail Insertion),徹底消除擴容時形成環形鏈結的風險。
二、 核心參數與空間換時間平衡
預設初始容量(Initial Capacity):⁠16⁠(必須維持為 2 的整數冪次方)。
負載因子(Load Factor):⁠0.75f⁠
官方依據泊松分佈(Poisson Distribution)計算出的平衡點,在空間利用率(避免過多空桶)與查詢時間(減少碰撞機率)之間取得折衷。
擴容閥值(Threshold):⁠Capacity × Load Factor⁠(預設為 16 \times 0.75 = 12)。
當容器中儲存的元素總數超過此閥值時,立即觸發 ⁠resize()⁠ 擴容為原本的 2 倍。
三、 為什麼容量必須維持 2^n?位元運算與取模效能
計算 Key 應放入陣列的哪一個桶位時,邏輯上屬於取模運算:⁠Index = Hash % Length⁠。
但 CPU 執行除法與取模(⁠%⁠)的運算週期,遠高於底層位元運算。JVM 巧妙運用了二進位數學特性:當 Length 為 2^n 時,⁠Hash % Length⁠ 等價於 ⁠Hash & (Length - 1)⁠
運算速度極大化:⁠&⁠(位元與)運算只需 1 個 CPU 指令週期,效能顯著高於取模。
桶位分佈均勻性:
2^n - 1 的二進位尾數全部為 ⁠1⁠(例如 16 - 1 = 15 \rightarrow 00001111)。
進行 ⁠&⁠ 運算時,桶位直接由 Hash 的低位數值決定;若容量不是 2^n,遮罩中會含有 ⁠0⁠,將導致特定桶位永遠無法被命中,加劇雜湊碰撞。
四、 雜湊擾動函數(Perturbation Function):化解低位碰撞
直接拿物件原生的 ⁠hashCode()⁠ 進行 ⁠& (Length - 1)⁠ 會產生隱憂:因為陣列初期容量通常不大(例如 16),僅有最低的 4 個 bit 參與運算,高位元特徵完全未被採用。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
無符號右移 16 位元(⁠h >>> 16⁠):將 32-bit 的高 16 位元推移到低 16 位元處。
異或運算(⁠^⁠):讓高半部與低半部的位元資訊深度混合。
核心目的:即便陣列長度較短,高位元的特徵也能折射至低位,有效降低碰撞機率。
Null Key 處理:HashMap 允許單一 ⁠null⁠ 作為 Key,強制固定指派存放於 Index 0 的桶位。
五、 樹化(Treeify)與退化(Untreeify)機制

  1. 轉為紅黑樹的兩大硬性條件(必須同時成立)
  2. 單一桶位的鏈結串列長度 \ge 8。
  3. 整個 HashMap 陣列總長度 \ge 64。
    若單桶長度達到 8,但陣列總長度小於 64,系統會優先選擇擴容陣列分散元素,而非直接進行樹化。
  4. 門檻選定依據(8 轉樹、6 退化)
    為什麼是 8?
    在均勻雜湊前提下,依泊松分佈計算,單一桶位碰撞達到 8 個元素的機率極低(小於千萬分之一)。樹化僅作為防禦極端惡意輸入或不良 HashCode 的兜底機制。
    為什麼退化是 6 而不是 7?
    設立長度為 2 的緩衝區間,防止頻繁增刪的邊界資料導致紅黑樹與鏈結串列之間發生高成本的頻繁震盪轉換。
    記憶體開銷考量:
    紅黑樹節點(⁠TreeNode⁠)的空間消耗是普通單向鏈結節點(⁠Node⁠)的兩倍以上,因此維持「非必要不樹化」原則。
    六、 擴容機制(Resize)最佳化
    在 JDK 8 中進行 2 倍擴容時,遷移元素無需重複調用耗時的 ⁠hash()⁠ 運算:
    元素的新位置僅有兩種可能:「原索引位置」或「原索引位置 + 舊容量」。
    僅需判斷 Hash 值對應舊容量新增的最高位(⁠hash & oldCap⁠)為 ⁠0⁠ 還是 ⁠1⁠:
    結果為 ⁠0⁠:維持在原桶位。
    結果為 ⁠1⁠:直接位移至 ⁠原桶位 + oldCap⁠。
    此機制不僅省略重算雜湊的開銷,也能自然且均勻地將原同桶元素平分至兩個新桶中。

上一篇
第十二天ArrayList 擴容演算法、拷貝開銷與LinkedList快取局部性劣勢
系列文
把Google AI當夥伴打造個人高效Coding工作流 共 13 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言