hashmap底层是数组+链表+红黑树协同,但红黑树仅在链表长度≥8且数组长度≥64时触发,基于泊松分布设定阈值以检测哈希异常,数组长度必为2的幂以保障位运算均匀散列。

HashMap底层用的是数组+链表+红黑树,但不是固定组合
Java 8 开始,HashMap 的底层确实是数组 + 链表 + 红黑树三者协同,但红黑树只在特定条件下触发——不是每个桶(bucket)默认就上树。核心逻辑是:数组存头节点,冲突时先链表,链表长度 ≥ 8 且数组长度 ≥ 64 才转红黑树。
这么设计是为了平衡查找效率和建树开销:短链表用遍历足够快,强行转树反而亏;而长链表在哈希碰撞严重时,O(n) 查找太慢,升成红黑树能压到 O(log n)。
-
static final int TREEIFY_THRESHOLD = 8:链表转树阈值,但只是“必要不充分”条件 -
static final int MIN_TREEIFY_CAPACITY = 64:数组最小容量要求,避免小数组频繁树化/退化 - 退化规则相反:红黑树节点 ≤ 6 时,会退回到链表,不是固定“7退6”这种模糊说法
为什么链表长度≥8才考虑转红黑树
这个数字不是拍脑袋定的,而是基于泊松分布推导出的——在理想哈希均匀前提下,链表长度达到 8 的概率已低于十万分之一。换句话说,真出现长度 ≥ 8 的链表,大概率说明发生了哈希碰撞异常(比如 key 的 hashCode() 写得差,或数据本身有强规律)。
所以它本质是个“异常检测开关”,不是性能调优参数。别想着把 TREEIFY_THRESHOLD 改成 4 来“提前优化”,反而会让正常场景多出一堆小红黑树,徒增内存和维护成本。
立即学习“Java免费学习笔记(深入)”;
- 改小阈值 → 更多树结构 → GC 压力增大、对象头开销变高、CPU 缓存局部性下降
- 改大阈值 → 长链表滞留时间变长 → 在极端碰撞场景下,
get()可能卡在 O(n) 而无法回落 - 真正该做的是重写 key 类的
hashCode()和equals(),让散列更均匀
数组长度为什么必须是2的幂次方
因为 HashMap 计算元素存放位置不用取模 %,而是用位运算 & (table.length - 1)。这要求 table.length 是 2 的幂,否则 length - 1 就不是全 1 的二进制数,位运算结果会丢失高位信息,导致大量索引集中到低地址段。
比如长度为 16(0b10000),length - 1 == 15 == 0b1111,和 hash 值做 & 运算,等价于取低 4 位,能均匀映射到 0~15;但如果长度是 15,length - 1 == 14 == 0b1110,最高位永远被清零,相当于浪费了整个高位散列能力。
- 扩容时始终翻倍(
oldCap ),保证始终是 2 的幂 - 构造函数传入初始容量,会被自动提升到最近的 2 的幂(如传 10 → 实际建 16 的数组)
- 别手动传 100 这种非 2 幂值指望“省空间”,反而因扩容多一次,更费时间和内存
红黑树节点退化回链表的时机容易被误解
很多人以为只要树里删到剩 6 个节点就立刻退化,其实不是。真实逻辑是:在 treeifyBin() 或 resize() 期间检查树节点数,只有 ≤ 6 才触发 untreeify();日常 remove() 不会实时退化,要等到下次扩容或树操作时才判断。
这意味着你可能看到一个只有 3 个节点的红黑树长期存在——它没毛病,也不浪费,因为退化本身也有开销,没必要为几个节点反复折腾。
- 退化不是“懒删除”的补救,而是 resize 时顺手做的清理动作
- 如果 map 长期不扩容,小红黑树就一直保持树形态,不会自动降级
- 这也解释了为什么 jstack 或 jmap 看到的
TreeNode实例数,不一定和当前有效 key 数严格对应
实际用的时候,与其纠结红黑树怎么转,不如盯住两件事:key 的 hashCode() 是否合理,以及预估容量是否够用。其他都是系统在背后默默扛着的细节。










