Java 8中HashMap.put()先调用key.hashCode()并执行扰动运算(h = key.hashCode()) ^ (h >>> 16),null键哈希值固定为0;下标通过hash & (table.length - 1)计算,要求table.length为2的幂;冲突时先比哈希再equals,链表长度≥8且容量≥64时树化,扩容触发条件是size > threshold,新容量翻倍,rehash时节点仅需判断高位bit决定落位原下标或原下标+旧容量。

put方法执行时如何计算key的哈希值
Java 8 中 HashMap.put() 第一步不是直接查表,而是调用 key.hashCode(),再对结果做一次扰动运算:(h = key.hashCode()) ^ (h >>> 16)。这个异或操作是为了让高位也参与下标计算,缓解低位冲突——尤其当数组长度是2的幂次(如默认16)时,只用低位容易导致大量key映射到同一桶。
注意:如果key为null,哈希值固定为0,不调用hashCode(),也不会抛NullPointerException。
哈希值怎么变成数组下标
下标不是直接取模,而是用位运算:index = hash & (table.length - 1)。前提是table.length必须是2的幂(扩容时强制保证),这样位运算等价于取余,但快得多。
常见误区:
- 不是hash % table.length,所以负哈希值不会出错(位运算是无符号逻辑)
- 如果你手动传入非2的幂长度(比如通过反射改table),下标计算会错乱,但JDK不提供这种构造方式
发生哈希冲突后怎么处理链表与红黑树
当table[index]已有节点时,进入冲突处理流程:
立即学习“Java免费学习笔记(深入)”;
- 先比哈希值,再用
equals()比key(顺序不能反,哈希不等可直接跳过equals) - 如果找到相同key,新value覆盖旧value,返回旧值
- 否则插入链表尾部(Java 8起用尾插法,避免多线程扩容时死链)
- 链表长度≥8且
table.length ≥ 64时,触发树化:TreeNode结构替换链表,转为红黑树 - 红黑树中插入仍基于
compareTo()或hashCode()比较,不依赖equals()顺序
树化条件两个都必须满足,缺一不可;反向退化(树转链表)阈值是节点数≤6。
扩容时机与rehash的代价在哪
触发扩容的条件是:size > threshold(即元素数超过“容量 × 负载因子”,默认0.75)。扩容不是简单复制,而是整个table重建,所有已有key都要重新计算下标、重新插入。
关键细节:
- 新容量是原容量左移1位(×2),所以仍是2的幂
- rehash过程中,每个桶里的节点要么留在原下标,要么落到原下标 + 旧容量位置(因为新长度多一位bit)
- 这个规律被用于Java 8的高效迁移,不用全部重新hash & (newLength-1),但你写自定义map时别盲目套用——它依赖扰动哈希和2的幂设计
真正慢的是大量rehash叠加哈希冲突,而不是单次put。所以初始化时预估size、设好初始容量,比后期优化put逻辑更有效。










