为什么在 Jdk 8 中 HashMap 要转成红黑树?

一则或许对你有用的小广告

欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于 Spring AI + Spring Boot3.x + JDK 21...点击查看;
  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot3.x + JDK 17...点击查看项目介绍; 演示链接: http://116.62.199.48:7070/;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/

面试考察点

  1. 性能优化意识:面试官不仅仅是想知道 "用了红黑树" 这个事实,更是想知道你是否理解链表查询 O(n) 的性能瓶颈,以及红黑树 O(log n) 的优化意义。

  2. 数据结构掌握:考察你是否了解红黑树的特点(自平衡、查询效率稳定),以及为什么选择红黑树而不是 AVL 树或 B+ 树。

  3. 安全防护认知:是否了解 HashDoS 攻击(恶意构造哈希冲突),以及红黑树如何作为防御手段。

核心答案

JDK 8 引入红黑树是为了 解决极端哈希冲突情况下链表查询性能退化的问题,将查询复杂度从 O(n) 优化到 O(log n):

对比项链表红黑树
查询复杂度O(n)O(log n)
插入复杂度O(1)(头插/尾插)O(log n)
空间开销每节点 1 个 next 指针每节点 left、right、parent、color
适用场景数据量小、冲突少数据量大、冲突多
8 个元素查询最坏 8 次最多 3 次

一句话总结:红黑树是 HashMap 在极端冲突场景下的 "保底方案",防止查询性能从 O(1) 退化为 O(n)。

深度解析

一、链表的性能瓶颈

上图展示了链表在极端哈希冲突场景下的性能问题。关键点:

  • 正常情况:哈希分布均匀,每个桶只有 0-2 个元素,查询接近 O(1)
  • 极端情况:所有元素哈希到同一个桶,链表长度 = 元素总数,查询退化为 O(n)
  • 现实场景:HashDoS 攻击、糟糕的哈希函数、特定数据分布都可能导致这种情况

二、红黑树的优化效果

上图对比了链表和红黑树的查询效率差异。核心优势:

  • 数量级提升:10 万元素的查询,从 10 万次比较降到 17 次,性能提升约 6000 倍
  • 稳定可预测:红黑树是自平衡二叉搜索树,不会退化为链表
  • 代价可控:虽然插入稍慢(需要旋转和变色),但查询是 HashMap 的核心操作

三、为什么选择红黑树而不是其他数据结构?

上图解释了为什么红黑树是最佳选择。关键理由:

  • AVL 树太严格:每次插入/删除可能需要多次旋转,维护成本高
  • 红黑树更实用:近似平衡已经够用,旋转次数有上限(插入最多 2 次旋转)
  • 工程实践验证:红黑树在 C++ STL、Linux 内核等广泛使用,成熟稳定

四、树化的触发条件

// HashMap 源码中的树化条件
static final int TREEIFY_THRESHOLD = 8;   // 链表 → 红黑树阈值
static final int MIN_TREEIFY_CAPACITY = 64; // 最小容量要求

// putVal 方法中的树化逻辑
for (int binCount = 0; ; ++binCount) {
    if ((e = p.next) == null) {
        p.next = newNode(hash, key, value, null);
        // 链表长度达到 8 时,尝试树化
        if (binCount >= TREEIFY_THRESHOLD - 1)
            treeifyBin(tab, hash);
        break;
    }
    // ...
}

// treeifyBin 方法
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    // 注意:如果容量 < 64,优先扩容而不是树化!
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        // ... 转换为红黑树
    }
}

两个条件必须同时满足

条件原因
链表长度 ≥ 8TREEIFY_THRESHOLD此时链表查询效率已经明显下降
数组容量 ≥ 64MIN_TREEIFY_CAPACITY容量小时,扩容比树化更有效

为什么容量 < 64 时优先扩容?

  • 扩容可以将元素分散到更多桶中,从根本上减少冲突
  • 树化增加了空间开销和代码复杂度,应该作为最后手段
  • 小容量时,扩容的性价比更高

五、树化与退树

上图展示了树化和退树的转换条件。设计亮点:

  • 滞后设计:阈值差 2,避免链表长度在 7-8 之间反复横跳
  • 类似思想:ArrayList 扩容 1.5 倍、线程池核心线程回收,都有类似的滞后设计

六、防御 HashDoS 攻击

这是红黑树引入的安全意义

上图解释了红黑树的安全价值。关键点:

  • HashDoS 攻击:2011-2012 年流行的攻击方式,针对哈希表算法复杂度漏洞
  • JDK 1.7 的脆弱性:完全依赖链表,攻击者可以让服务器 CPU 100%
  • JDK 1.8 的防御:红黑树将攻击影响从 O(n) 降到 O(log n),攻击成本提高数千倍

面试高频追问

  1. 为什么树化阈值是 8,退树阈值是 6?

    • 8 是泊松分布下概率约千万分之一的值,说明已经异常
    • 差 2 是为了避免频繁转换的性能抖动
  2. 为什么不用 AVL 树?

    • AVL 树平衡要求更严格,插入/删除时旋转次数更多
    • 红黑树的近似平衡对查询性能影响很小,但维护成本更低
  3. 容量小于 64 时为什么优先扩容而不是树化?

    • 扩容可以从根本上减少冲突
    • 树化有额外空间开销,应该作为最后手段

常见面试变体

  • "HashMap 中链表转红黑树的条件是什么?"
  • "红黑树和 AVL 树有什么区别?为什么 HashMap 选红黑树?"
  • "什么是 HashDoS 攻击?HashMap 如何防御?"

记忆口诀

树化条件:链表 8、容量 64,两个条件要记牢。

性能提升:链表 O(n) 太慢了,红黑树 O(log n) 救场,HashDoS 也不慌。

总结

JDK 8 引入红黑树是为了解决极端哈希冲突导致的链表查询性能退化问题,将复杂度从 O(n) 优化到 O(log n)。树化需要满足链表长度 ≥ 8 且容量 ≥ 64 两个条件。红黑树相比 AVL 树维护成本更低,同时还能有效防御 HashDoS 攻击。记住:树化是兜底方案,不是常规优化