hashmap在jdk 8中采用数组+链表/红黑树混合结构,链表长度≥8且数组长度≥64才转红黑树,是时间与空间权衡结果;hash()通过高16位异或低16位扰动哈希值,提升低位参与度以优化分布。

HashMap 在 JDK 8 中不是简单的数组+链表,而是「数组 + 链表/红黑树」的混合结构,核心在于 put 时根据桶内节点数动态切换存储形态。
为什么链表长度 ≥ 8 且数组长度 ≥ 64 才转红黑树?
这是时间和空间的权衡结果。链表查找是 O(n),红黑树是 O(log n),但树节点比普通 Node 占用更多内存,且小规模数据下常数开销反而更高。
-
8是泊松分布推导出的阈值:在负载因子0.75下,链表长度达到 8 的概率已低于千万分之一,说明此时大概率是哈希碰撞异常(如 key 的hashCode()实现不合理) -
64是避免过早树化:如果数组太小(比如只有 16),大量元素集中在几个桶里,可能只是扩容没跟上,此时应优先扩容而非树化 - 两个条件必须同时满足,缺一不可;只满足一个会触发
resize()或继续走链表逻辑
hash() 函数到底做了什么?
JDK 8 的 hash() 不是直接返回 key.hashCode(),而是高 16 位异或低 16 位:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}这么做是为了让哈希值的低位也参与数组索引计算。因为实际取桶位置用的是 (n - 1) & hash(n 是数组长度,必为 2 的幂),若不扰动,仅靠低几位决定桶位置,会导致高位信息丢失、哈希分布不均、大量碰撞。
立即学习“Java免费学习笔记(深入)”;
扩容时链表怎么拆分?为什么不用重新 hash?
扩容从旧数组长度 oldCap 变为 oldCap ,即二进制多一位。关键点在于:同一个桶里的节点,扩容后只会去两个固定位置——原位置或原位置 + <code>oldCap。
- 因为新桶索引是
(newCap - 1) & hash,而newCap = oldCap * 2,所以新索引只比旧索引多看 hash 的第log2(oldCap)位 - 该位为 0 → 节点留在原位置;为 1 → 节点移到
原索引 + oldCap - 因此只需判断该位,无需重新调用
key.hashCode()或hash(),性能更好
并发场景下 put 为什么会死循环?
JDK 7 中的头插法 + 多线程扩容未加锁,会导致链表逆序和环形引用。例如线程 A、B 同时对同一桶扩容,各自构建新链表时把对方刚插入的节点又接回去,最终形成环。
JDK 8 已改为尾插法,且扩容时通过 ForwardingNode 协作控制,单个桶的迁移是原子的,消除了死循环问题。但 HashMap 本身仍**不是线程安全的**——并发 put 可能导致数据覆盖、丢失或 size 计算错误。
真正要注意的是:哪怕用了 JDK 8,只要没做同步或换用 ConcurrentHashMap,就别在多线程里裸用 HashMap。










