作為 Java 面試「必考王」與日常開發「高頻主力」,HashMap 看似簡單卻暗藏大量精妙設計。本文基於 JDK 8 源碼,從「鏈表散列」數據結構、put/get 核心流程、hash 擾動算法、indexFor 定位邏輯、resize 擴容遷移、性能參數調優、Fail-Fast 機制六大維度,帶你徹底攻克這個核心集合類。
HashMap 底層維護一個 Node<K,V>[] table 數組,每個槽位(Bucket)掛載鏈表或紅黑樹:
transient Node<K,V>[] table;
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next; // 鏈表指針
// ... 省略 treeNode 字段(JDK 8 引入)
}
JDK 8 關鍵優化:當鏈表長度 ≥ 8 且數組長度 ≥ 64 時,自動轉為紅黑樹(TreeNode),將最壞查找從 O(n) 降為 O(log n),有效防禦 Hash 洪水攻擊。
public V put(K key, V value) {
if (key == null) return putForNullKey(value); // null key 固定放 table[0]
int hash = hash(key.hashCode()); // 關鍵:二次擾動
int i = indexFor(hash, table.length); // 定位槽位
for (Node<K,V> e = table[i]; e != null; e = e.next) {
if (e.hash == hash && (e.key == key || key.equals(e.key))) {
V old = e.value;
e.value = value;
e.recordAccess(this);
return old; // key 相同則覆蓋 value
}
}
modCount++;
addEntry(hash, key, value, i); // 頭插法入鏈
return null;
}
hash 相同 && (引用相同 || equals 為 true) 才視為同一 keyhash=0, index=0public V get(Object key) {
if (key == null) return getForNullKey();
int hash = hash(key.hashCode());
for (Node<K,V> e = table[indexFor(hash, table.length)];
e != null; e = e.next) {
if (e.hash == hash && (e.key == key || key.equals(e.key)))
return e.value;
}
return null;
}
本質:hash 定位槽位 → equals 遍歷鏈表/樹確認 key。
static final int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
設計意圖:將 hashCode() 的高位信息混入低位,因為 indexFor 只用低位(length-1 掩碼)。若高位變化、低位不變,無擾動會導致嚴重碰撞。
static int indexFor(int h, int length) {
return h & (length - 1); // length 必須是 2 的冪次
}
為什麼 length 必須是 2 的冪?
| length | length-1 (二進制) | h=8 (01000) | h=9 (01001) | 結果 |
|---|---|---|---|---|
| 15 (非 2 冪) | 1110 | 0100 | 0100 | 碰撞! 且最低位永遠為 0,浪費 1/2 空間 |
| 16 (2 冪) | 1111 | 0100 | 0101 | 無碰撞,空間利用率 100% |
結論:2^n - 1 低位全 1,& 運算等價於取模,且保留所有低位信息,配合 hash() 高位混入,極大降低碰撞概率。
size >= threshold = capacity * loadFactor,默認 loadFactor=0.75,初始 capacity=16 → 閾值 12。
void resize(int newCapacity) {
Node<K,V>[] newTable = new Node[newCapacity];
for (Node<K,V> e : table) {
while (e != null) {
Node<K,V> next = e.next;
// 關鍵優化:重新計算索引,或利用 (e.hash & oldCap) == 0 判斷留在原槽/遷移到 newIndex
int newIdx = e.hash & (newCapacity - 1);
e.next = newTable[newIdx];
newTable[newIdx] = e; // 保持原相對順序(尾插效果)
e = next;
}
}
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
JDK 8 遷移優化:利用 e.hash & oldCap 判斷高位變化,避免重新計算 hash(),且保持鏈表順序不反轉,徹底解決 JDK 7 併發成環 Bug。
| 參數 | 默認值 | 調優建議 |
|---|---|---|
| initialCapacity | 16 | 預估元素數量 / 0.75,避免多次擴容(如預計 1000 個 → new HashMap(1365)) |
| loadFactor | 0.75 | 空間換時間:0.5 更稀疏、查找更快;0.9 更緊湊、內存省但碰撞增。極少修改 |
| threshold | capacity * loadFactor | 只讀字段,無法直接設定 |
最佳實踐:new HashMap<>((int)(expectedSize / 0.75f) + 1) 一次性分配足夠容量。
modCount 記錄結構性修改次數(put/remove/resize 遞增)expectedModCount = modCountnext() 檢查 modCount != expectedModCount → 拋 ConcurrentModificationExceptionhashCode/equalshashCode() 與 equals(),且滿足「equal 則 hash 相同」契約ConcurrentHashMap 或 Collections.synchronizedMapinitialCapacity,避免頻繁 resize 導致 GC 噴峰key.hashCode() 返回常數(如未重寫)導致退化為鏈表,性能崩塌HashMap 以「數組 + 鏈表/紅黑樹」為骨架,通過 hash() 高位混入擾動、indexFor() 位運算定位、resize() 2 倍擴容遷移、loadFactor 空間時間權衡,構建出極致的鍵值存取性能。掌握其源碼邏輯,不僅是面試通關鑰匙,更是寫出高性能、低延遲 Java 代碼的內功心法。下一篇我們將挑戰 ConcurrentHashMap 的分段鎖與 CAS 併發藝術。