hashmap在jdk8中链表长度达8且表容量≥64才转红黑树,因小表扩容比树化更高效;退化阈值为6以避免震荡;treenode是node子类,通过instanceof动态分发操作。

为什么HashMap在JDK8里链表长度到8就转红黑树
不是“刚好8就转”,而是当某个桶(bucket)的链表长度达到阈值 TREEIFY_THRESHOLD = 8,且整个哈希表容量(table.length)≥64时,才会触发链表转红黑树。小于64会先尝试扩容,因为扩容成本通常低于树化开销。
这么设计的核心原因是:链表查找平均时间复杂度是 O(n),而红黑树稳定在 O(log n);但红黑树节点更重、插入删除逻辑复杂,小数据量下反而不如链表快。JDK作者通过大量基准测试发现,链表长度 ≥8 且表足够大时,树化才真正带来收益。
-
treeifyBin()是实际触发树化的入口方法,它先检查容量,再调用treeify() - 红黑树节点类型是
TreeNode,继承自Node,但额外包含parent、left、right、red字段 - 树化过程不是重建整棵树,而是遍历原链表,逐个插入到红黑树中,并重平衡
链表退化回数组(untreeify)发生在什么时机
红黑树不会永远存在——当某个树化桶在 resize 后节点数 ≤6,就会退化回链表,调用的是 untreeify() 方法。注意这个阈值(6)和树化阈值(8)不一致,中间留了2的缓冲区间,避免频繁树化/退化震荡。
退化逻辑很简单:遍历 TreeNode 链(红黑树内部也维护了双向链表关系),新建普通 Node 节点,串成链表。没有平衡操作,也不涉及颜色或父子指针清理。
- 退化只发生在扩容(
resize())过程中,不会在remove()或put()单次操作里主动触发 - 如果只是删掉几个元素让节点数变成7或6,树结构仍保留,直到下次 resize 再判断
-
UNTREEIFY_THRESHOLD = 6是硬编码常量,不可配置
红黑树节点怎么和普通Node共存于同一个数组槽位
关键在于 Node 和 TreeNode 都实现了 Map.Entry 接口,且 HashMap 的 table 数组类型是 Node[]。由于 TreeNode 是 Node 的子类,所以能直接存进去——这是典型的多态应用。
所有读写操作(如 get()、putVal())都先通过 tab[i] 拿到头节点,再根据其实际运行时类型分发逻辑:
- 如果是
Node,走链表遍历 - 如果是
TreeNode,调用getTreeNode()进入红黑树查找路径 - 类型判断用的是
instanceof TreeNode,不是字段标记
实际调试时怎么确认某个桶到底是链表还是红黑树
最直接的方式是打断点进 get() 或 put(),观察 tab[i] 的实际类型;或者用 IDE 的「Evaluate Expression」查看 table[索引].getClass().getSimpleName()。
也可以写个小验证片段:
Map<string integer> map = new HashMap(); for (int i = 0; i <p>注意:除非你手动构造大量哈希冲突(比如用相同 <code>hashCode()</code> 的对象),否则日常使用几乎看不到红黑树——JDK8默认初始容量16,加载因子0.75,意味着12个元素就扩容,冲突概率被有效稀释。</p> <p>真正容易忽略的是:树化依赖两个条件同时满足,只盯着链表长度看会误判;另外,<code>TreeNode</code> 在序列化时会被自动转成普通 <code>Node</code> 链表,反序列化后仍是链表,不恢复树结构。</p></string>










