
HashMap 在 JDK 8 中已不是简单的数组+链表结构,而是「数组 + 链表/红黑树」的混合实现,且扩容、哈希计算、树化等逻辑都围绕性能与并发安全的权衡展开。直接读 HashMap 源码容易被细节绕晕,关键要抓住三个动作:put() 怎么定位桶、resize() 怎么搬数据、treeifyBin() 什么条件下转红黑树。
哈希值怎么算?为什么用 (h = key.hashCode()) ^ (h >>> 16)
Java 的 hashCode() 返回的是 32 位 int,但 HashMap 的数组长度总是 2 的幂(如 16、32、64),取索引用的是 tab[(n - 1) & hash],即低位掩码。如果只用原始 hashCode(),高位变化对索引几乎没影响,容易导致哈希冲突集中——尤其当 key 是连续整数或字符串前缀相同时。
所以 JDK 8 引入了这行扰动:
int h = key.hashCode(); h ^= (h >>> 16);它把高 16 位异或到低 16 位,让高位信息也参与索引计算,显著改善低位分布。这不是加密,是低成本的哈希扩散。
- 不要自己重写
hashCode()却忽略这一点:若返回值集中在低位(比如只用id % 100),再好的扰动也救不了 -
String的hashCode()本身已较均匀,但仍有优化空间;而Integer的hashCode()就是自身值,必须靠这步扰动
put() 执行时,怎么决定是链表插入还是树化?
不是一上来就树化,也不是达到某个固定 size 就转。真正触发条件有两层检查:
- 当前桶(bin)的链表长度 ≥
TREEIFY_THRESHOLD(默认 8) - 整个
HashMap的size≥MIN_TREEIFY_CAPACITY(默认 64)
第二条常被忽略:如果总容量还不到 64,说明数组太小,优先选择 resize() 扩容(翻倍),而不是树化。因为扩容后链表自然分散,性价比更高。只有在大容量下仍出现长链,才值得树化。
立即学习“Java免费学习笔记(深入)”;
树化入口是 treeifyBin(),它先检查是否满足上述两个条件,再调用 treeify() 把 Node 链表转为 TreeNode 红黑树节点,并重排结构。
扩容时 resize() 怎么处理红黑树和链表?
扩容不是简单复制,而是重新哈希再分配。由于新容量是旧容量的 2 倍,每个元素的新索引只有两种可能:原位置 lo 或 lo + oldCap(因为 (n*2 - 1) & hash 相比 (n - 1) & hash 多了一位判断位)。
对链表:遍历一次,按新索引拆成两个子链(loHead / hiHead),再分别挂到新数组对应位置。
对红黑树:同样拆成两个子树,但会进一步判断——如果拆分后任一子树节点数 ≤ UNTREEIFY_THRESHOLD(默认 6),就退化回链表;否则保留红黑树结构。
- 注意:红黑树节点迁移不涉及旋转或颜色重排,只是“拆分+降级/保留”,所以开销可控
- 如果频繁扩容又频繁树化,说明初始容量设得太小,或 key 的
hashCode()实现极差
为什么 get() 查找快?但多线程下不能直接用?
get() 快,是因为:① 定位桶是 O(1) 位运算;② 链表平均长度被控制在 1 以内(负载因子 0.75);③ 树化后查找是 O(log n)。但所有这些都建立在「结构稳定」前提下。
多线程 put 可能导致死循环(JDK 7 的典型问题)、数据覆盖、甚至 get() 返回 null(明明 put 过)。JDK 8 虽修复了死循环,但依然不保证线程安全:比如两个线程同时触发 resize(),可能一个扩容完,另一个把旧节点插到新数组里,造成数据丢失。
- 需要并发场景,请用
ConcurrentHashMap,它通过分段锁(JDK 7)或 CAS + synchronized(JDK 8+)保障安全 -
HashMap的迭代器是 fail-fast 的,一旦检测到结构性修改(如其他线程 put),立刻抛ConcurrentModificationException
真正难啃的不是代码行数,而是每处设计背后的取舍:比如为何树化阈值是 8 而不是 7 或 9?Oracle 的解释是基于泊松分布,链表长度 ≥ 8 的概率已低于千万分之一——这意味着绝大多数桶根本不会树化。这种统计直觉,比背源码更重要。









