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