HashMap.put()执行时先计算扰动哈希值,再通过位运算定位桶索引;若桶为空则直接插入,否则遍历链表或红黑树判断是否覆盖,链表长度≥8且数组长度≥64时树化,扩容阈值为容量×负载因子。

put() 方法执行时发生了什么
Java 中 HashMap.put() 不是简单地把键值对塞进数组,而是一套带哈希计算、冲突处理和动态扩容的完整流程。核心逻辑在 JDK 8+ 中已从“链表头插”改为“尾插”,且当链表长度 ≥ 8 且桶数组长度 ≥ 64 时,会转为红黑树。
哈希值计算与索引定位的关键细节
HashMap 对 key.hashCode() 做了二次扰动:(h = key.hashCode()) ^ (h >>> 16),目的是让高位也参与取模运算,降低低位相同导致的哈希碰撞概率。最终桶索引通过 tab[(n - 1) & hash] 计算(n 是数组长度,必须是 2 的幂),这比取模 % n 更快,但要求容量始终是 2 的整数次幂。
- 如果
key为null,哈希值固定为 0,总是映射到索引 0 的桶 - 自定义类作
key时,必须重写hashCode()和equals(),否则可能无法正确get() - 哈希值相同不等于
equals()成立,所以后续仍需调用equals()判断是否覆盖
链表转红黑树的触发条件容易被误读
常见误解是“只要链表长度 ≥ 8 就转树”,实际需要同时满足两个条件:table.length >= 64 且 binCount >= 8(即遍历链表后计数达到 8)。若数组太小(如初始容量 16),即使某桶链表很长,也不会树化,而是先触发扩容。
- 扩容阈值默认是
capacity * loadFactor(初始为 16 × 0.75 = 12) - 扩容后数组长度翻倍,所有元素重新哈希再分配,开销较大,应根据预估数据量合理设置初始容量
- 红黑树节点
TreeNode比普通Node占用更多内存,树化是空间换查询时间的权衡
并发场景下 put() 的危险行为
HashMap 本身不是线程安全的。多线程同时 put() 可能导致死循环(JDK 7 链表头插 + 扩容重哈希时)、数据丢失或 get() 返回 null 等未定义行为。这不是 bug,是设计使然。
立即学习“Java免费学习笔记(深入)”;
- JDK 8 中死循环问题已修复(改用尾插),但并发
put()仍可能丢失更新 - 如需线程安全,优先选
ConcurrentHashMap;若只是读多写少,可考虑Collections.synchronizedMap() - 禁止在 foreach 或迭代器遍历时调用
put(),会抛ConcurrentModificationException
// 示例:手动模拟 put 过程的关键判断点 int hash = hash(key.hashCode()); // 扰动哈希 int i = (table.length - 1) & hash; Nodep = table[i]; if (p == null) { table[i] = newNode(hash, key, value, null); // 直接插入 } else if (p.hash == hash && Objects.equals(p.key, key)) { p.value = value; // 覆盖旧值 } else if (p instanceof TreeNode) { e = ((TreeNode )p).putTreeVal(this, tab, hash, key, value); // 树中插入 } else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 因为从 0 开始计数 treeifyBin(tab, hash); // 满足条件才尝试树化 break; } if (e.hash == hash && Objects.equals(e.key, key)) break; p = e; } }
真正影响性能的往往不是单次 put(),而是哈希分布不均、频繁扩容或误用线程不安全场景。这些地方一旦出问题,现象隐蔽,排查成本远高于提前设好初始容量或选对并发容器。










