| 版本 | 底層結構 | 碰撞處理 | 擴容機制 | 關鍵閾值 |
|---|---|---|---|---|
| JDK 7 | 數組 + 單向鏈表 | 頭插法/尾插法(頭插易死循環) | rehash 全量遷移,單線程不安全 | 負載因子 0.75,無樹化 |
| JDK 8 | 數組 + 鏈表 + **紅黑樹** | 尾插法保持順序,**鏈表長 ≥ 8 樹化** | 高位擴容優化(無需重新計算 hash),支持並發擴容輔助 | TREEIFY_THRESHOLD=8, UNTREEIFY_THRESHOLD=6, MIN_TREEIFY_CAPACITY=64 |
// Node.java 核心邏輯片段
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) {
resize(); // 數組太小,優先擴容而非樹化
return;
}
// ... 將鏈表轉換為 TreeNode 紅黑樹
}
TREEIFY_THRESHOLD (8)。MIN_TREEIFY_CAPACITY (64)。若容量不足,優先 resize() 擴容分散碰撞。UNTREEIFY_THRESHOLD (6),紅黑樹退化為鏈表。TreeNode<K,V> 繼承 Node<K,V>static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // 紅黑樹父節點
TreeNode<K,V> left; // 左子節點
TreeNode<K,V> right; // 右子節點
TreeNode<K,V> prev; // 鏈表前驅(用於遍歷/擴容遷移)
boolean red; // 紅黑樹顏色
// treeify/split/rotate/balance 等紅黑樹操作...
}
// 必須重新計算每個 key 的 hash & (newCap - 1)
for (Entry<K,V> e : oldTable) {
while (e != null) {
Entry<K,V> next = e.next;
int i = hash(e.key) & (newCap - 1); // 重新計算索引
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
// 核心邏輯:e.hash & oldCap 判斷高位是否為 1
Node<K,V>[] newTab = new Node[newCap];
for (Node<K,V> e : oldTab) {
if (e == null) continue;
if (e.next == null) { // 單節點直接遷移
newTab[e.hash & (newCap - 1)] = e;
} else if (e instanceof TreeNode) { // 紅黑樹分裂
((TreeNode<K,V>)e).split(newTab, oldCap, newCap);
} else { // 鏈表分裂:保持原序(尾插法優勢)
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) { // 高位為 0,留在原索引
if (loTail == null) loHead = e;
else loTail.next = e;
loTail = e;
} else { // 高位為 1,遷移到原索引 + oldCap
if (hiTail == null) hiHead = e;
else hiTail.next = e;
hiTail = e;
}
e = next;
} while (e != null);
if (loTail != null) { loTail.next = null; newTab[j] = loHead; }
if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; }
}
}
優勢:無需重新計算 hash,僅判斷高位比特,**效能大幅提升**,且鏈表順序不反轉(避免 JDK 7 並發擴容死循環)。
hashCode() 返回固定值(極端碰撞)或連續值(正常分布)。map.get(key) 循環訪問。得益於擴容優化、內存佈局緊湊、JIT 友好。
hashCode() 恒返 0):複雜度從 O(n) 降為 O(log n)// 惡意 Key
class EvilKey implements Comparable<EvilKey> {
private final int id;
@Override public int hashCode() { return 0; } // 所有 Key 同一桶
@Override public int compareTo(EvilKey o) { return Integer.compare(this.id, o.id); }
// equals 正確實現...
}
| Map 大小 | JDK 7 get 耗時 (ns) | JDK 8 get 耗時 (ns) | 倍數 |
|---|---|---|---|
| 1,000 | ~1,200 | ~150 | 8x |
| 10,000 | ~12,000 | ~300 | 40x |
| 100,000 | ~120,000 | ~500 | 240x |
| 1,000,000 | ~1,200,000 | ~700 | 1,700x |
結論:JDK 8 樹化機制將惡意碰撞 DoS 攻擊面從線性降為對數,**有效防禦 HashDoS**。
hashCode() 相同且 equals() 不同時,紅黑樹需比較大小決定左右子樹。Comparable<K> 可獲得最佳樹化效能**Class>.getName() 比較(不穩定),甚至退化為鏈表行為。// 良好的 Key 實現
class GoodKey implements Comparable<GoodKey> {
private final long id;
@Override public int hashCode() { return Long.hashCode(id); }
@Override public boolean equals(Object o) { ... }
@Override public int compareTo(GoodKey o) { return Long.compare(this.id, o.id); }
}
put(key, value) ↓ hash(key) → 索引 i = (n-1) & hash ↓ tab[i] 為 null? → 是:newNode 放入 → 結束 ↓ 否 key 相同(hash 同且 equals 真)? → 是:覆蓋舊值 → 結束 ↓ 否 節點是 TreeNode? → 是:treePutTreeVal (紅黑樹插入) → 結束 ↓ 否 (鏈表) 遍歷鏈表尾部插入 (尾插法) ↓ 鏈表長度 ≥ 8? → 是:treeifyBin (嘗試樹化) → 結束 ↓ 否 結束
new HashMap<>(expectedSize * 4 / 3 + 1) 避免多次擴容。hashCode()/equals()/Comparable:業務主鍵類必須三者一致。ConcurrentHashMap:JDK 8 CAS + synchronized 細粒度鎖,JDK 1.7 Segment 鎖。WeakHashMap:Key 為弱引用,GC 時自動清理。LinkedHashMap (訪問序/插入序) 或 TreeMap (自然序/Comparator)**HashMap 從 JDK 7 到 JDK 8 的演進,是**演算法與工程結合的經典案例**:
理解這些機制,不僅能應對面試,更能在設計高併發緩存、限流計數器、分佈式鎖 Key 等場景時,做出正確的容器選型與參數調優決策。