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)路径
-
计算桶索引,若该桶为空 → 直接挂节点。
-
若是链表桶:
-
正常追加;计数到 8 时:
- 若表长 < 64 → resize(扩容,见下);
- 否则执行 treeifyBin → 把桶内节点就地转换为一棵红黑树。
-
-
若是树桶:走 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) 常见问题 / 排坑
-
树化后顺序是不是按 key 排序? 不是。遍历顺序仍是"插入顺序受 rehash/扩容影响"的 HashMap 逻辑;要顺序请用 LinkedHashMap。
-
我的桶老在 7↔8 来回切? 不会。因为有 6 的退化阈值(迟滞),并且扩容会把桶长度拉低。
-
大量不同 key 却同一个 hashCode() 会怎样?****
- 容量≥64 时会树化,把最坏复杂度降到 O(log n) ;
- 更根本的修正是:修好 hashCode/equals,并保证 key 不可变。
-
并发下树化安全吗? HashMap 不是线程安全;并发写可能结构损坏(JDK8 已修复早年"循环链表"问题,但仍不保证并发安全)。多线程请用 ConcurrentHashMap。
-
能手动把某桶变树吗? 不要;让策略自动触发。强行树化只会增加不必要的开销。
8) 面试速答(20 秒版)
JDK 8 的 HashMap 当同一桶元素数 ≥ 8 且表长 ≥ 64 时,把该桶从链表树化为红黑树 ,把最坏查找从 O(n) 降到 O(log n) ;当桶内元素 ≤ 6 (删除或扩容拆分后),退回链表 。64 的门槛让小表优先扩容分散 ,8/6 形成迟滞 避免抖动;扩容时树桶也按位lo/hi 折分 ,总体搬迁 O(n) 。