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)

相关推荐
Lee川15 小时前
优雅进化的JavaScript:从ES6+新特性看现代前端开发范式
javascript·面试
Lee川18 小时前
从异步迷雾到优雅流程:JavaScript异步编程与内存管理的现代化之旅
javascript·面试
晴殇i20 小时前
揭秘JavaScript中那些“不冒泡”的DOM事件
前端·javascript·面试
绝无仅有20 小时前
Redis过期删除与内存淘汰策略详解
后端·面试·架构
绝无仅有21 小时前
Redis大Key问题排查与解决方案全解析
后端·面试·架构
AAA梅狸猫1 天前
Looper.loop() 循环机制
面试
AAA梅狸猫1 天前
Handler基本概念
面试
Wect1 天前
浏览器缓存机制
前端·面试·浏览器
掘金安东尼1 天前
Fun with TypeScript Generics:玩转 TS 泛型
前端·javascript·面试
掘金安东尼1 天前
Next.js 企业级落地
前端·javascript·面试