一、 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)機制