HashMap树化:桶内≥8 且容量≥64 → 红黑树;≤6 退回链表

1) 什么时候"桶"会从链表变红黑树?

三个常量(JDK 8 源码同名):

  • TREEIFY_THRESHOLD = 8:桶内元素数 ≥ 8 才考虑树化。

  • UNTREEIFY_THRESHOLD = 6:桶内元素数 ≤ 6 时会"退树为链 "(避免抖动有迟滞,所以是 6 而不是 7)。

  • MIN_TREEIFY_CAPACITY = 64:只有当 整张表容量 ≥ 64 时,才允许树化 ;否则优先扩容(把冲突摊薄更划算)。

因此:

桶内≥8 且表长≥64 → 红黑树;≤6 → 退回链表

(=8、=7、=6 之间由插入/删除/扩容时的逻辑与迟滞共同决定。)


2) 为什么要树化?(从 DoS 防护到最坏复杂度)

  • 普通链表桶:查找最坏 O(n) 。一旦哈希被恶意构造(HashDoS)或 hashCode 分布差,可能出现长链
  • 红黑树桶:查找最坏 O(log n) ,把极端情况"锯掉尖"。
  • "8/6 的阈值"来自泊松分布评估 + 工程经验 :在 负载因子 ~0.75 、哈希均匀时,桶长≥8 的概率极低;把树化门槛放在 8 基本只在"真的有问题"的时候触发。用 6 做回退阈值形成迟滞,避免反复树↔链抖动。
  • "64 的全局门槛"是为了先扩容 而不是过早树化:当表还小,扩容能更有效地减少碰撞,比在小表上维护树更省时省内存。

3) 树化/退化是怎么做的?(核心方法与流程)

HashMap.Node<K,V> 是链表节点;树化后换成 TreeNode<K,V>(红黑树节点,继承自 LinkedHashMap.Entry)。

插入(put)路径

  1. 计算桶索引,若该桶为空 → 直接挂节点。

  2. 若是链表桶:

    • 正常追加;计数到 8 时:

      • 若表长 < 64resize(扩容,见下);
      • 否则执行 treeifyBin → 把桶内节点就地转换为一棵红黑树。
  3. 若是树桶:走 putTreeVal,按树的比较逻辑 (先比 hash,再尝试 Comparable,再用类名/identityHash 做 tie-break)插入并红黑平衡

删除(remove)路径

  • 链表:常规删除。

  • 树:removeTreeNode 删除后若当前桶节点数 ≤ 6 ,调用 untreeify 退化为链表;否则做红黑树删除平衡

比较顺序不是按"键的大小" :HashMap 的树不是 "按 key 的自然顺序的有序树",而是先按 hash 值 决定方向;只有当 hash 相等时,才尝试 Comparable,再不行用类名字符串/System.identityHashCode 作为稳定的 tie-break,保证可构建平衡树。


4) 扩容与树桶:split一刀两段

扩容(容量翻倍)时,桶按 (hash & oldCap) 分成低/高两段

  • 链表桶:拆成两条子链挂到原索引原索引+oldCap(保持相对顺序)。

  • 树桶 :TreeNode.split 同样按位拆分为 lo/hi 两组;

    • 组内节点数 ≤ 6 → 直接退链

    • 否则维持为树。

      这保证扩容总体 O(n) ,同时配合迟滞阈值减少不必要的"树↔链"抖动。


5) 复杂度 & 代价(要不要追求"全树化"?)

  • 查找 :链表时间均摊接近 O(1) ;超长链才会劣化到 O(n) 。树化后最坏 O(log n)
  • 更新成本 :维护红黑树需要旋转/着色,插入/删除更贵;在冲突不多时,链表反而更快。
  • 内存:TreeNode 比 Node 大(多了父/左/右/颜色等字段)。
  • 结论不要人为降低树化门槛 ;默认策略是在极端碰撞时用树兜底,平时用链表更省。

6) 与"扰动 hash + 2 的幂容量"的协同

  • 扰动 h ^ (h >>> 16):把高位信息混到低位,减少只看低位取桶带来的集中碰撞。
  • 容量 2 的幂:索引 hash & (n-1);扩容时只看一位决定"留原位/加 oldCap",搬迁便宜。
  • 这两点把碰撞概率扩容成本 压到低位,树化只在确实需要时发生

7) 常见问题 / 排坑

  1. 树化后顺序是不是按 key 排序? 不是。遍历顺序仍是"插入顺序受 rehash/扩容影响"的 HashMap 逻辑;要顺序请用 LinkedHashMap。

  2. 我的桶老在 7↔8 来回切? 不会。因为有 6 的退化阈值(迟滞),并且扩容会把桶长度拉低。

  3. 大量不同 key 却同一个 hashCode() 会怎样?****

    • 容量≥64 时会树化,把最坏复杂度降到 O(log n)
    • 更根本的修正是:修好 hashCode/equals,并保证 key 不可变
  4. 并发下树化安全吗? HashMap 不是线程安全;并发写可能结构损坏(JDK8 已修复早年"循环链表"问题,但仍不保证并发安全)。多线程请用 ConcurrentHashMap。

  5. 能手动把某桶变树吗? 不要;让策略自动触发。强行树化只会增加不必要的开销。


8) 面试速答(20 秒版)

JDK 8 的 HashMap 当同一桶元素数 ≥ 8表长 ≥ 64 时,把该桶从链表树化为红黑树 ,把最坏查找从 O(n) 降到 O(log n) ;当桶内元素 ≤ 6 (删除或扩容拆分后),退回链表 。64 的门槛让小表优先扩容分散 ,8/6 形成迟滞 避免抖动;扩容时树桶也按位lo/hi 折分 ,总体搬迁 O(n)

相关推荐
我是华为OD~HR~栗栗呀8 小时前
24届-Python面经(华为OD)
java·前端·c++·python·华为od·华为·面试
Takklin9 小时前
Java 面试笔记:深入理解接口
后端·面试
南北是北北9 小时前
HashMap扩容:翻倍 + 低/高位链分裂(O(n))
面试
笔尖的记忆10 小时前
【前端架构和框架】react中Scheduler调度原理
前端·面试
南北是北北12 小时前
类型推断、重载与桥方法
面试
程序员清风12 小时前
滴滴二面:MySQL执行计划中,Key有值,还是很慢怎么办?
java·后端·面试
南北是北北13 小时前
泛型变体与通配符(PECS)+ 集合
面试
shepherd11113 小时前
JDK 8钉子户进阶指南:十年坚守,终迎Java 21升级盛宴!
java·后端·面试
南北是北北13 小时前
界类型参数、递归边界与交叉类型
面试