Java8 为什么这里把key的hashcode取出来,然后把它右移16位,然后取异或?

文章目录

【深入源码】图解 HashMap 扰动函数:为什么要把高位"揉"进低位?

在阅读 HashMap 源码时,很多小伙伴会被 (h = key.hashCode()) ^ (h >>> 16) 这一行代码困惑。为什么要右移 16 位?为什么要进行异或?本文通过一个具体的案例,带你像剥洋葱一样看透这个"扰动函数"的奥秘。


1. 核心矛盾:被浪费的"40亿"

hashCode 是一个 32 位的整数,范围高达 40 亿。但现实中,我们的初始数组长度往往只有 16。

在计算下标时,公式为:(n - 1) & hash

如果数组长度为 16,计算过程只取决于 最后 4 位。这意味着,即便高位有再大的差异,只要低 4 位相同,就一定会发生哈希碰撞。


2. 案例实战:如果不"扰动"会发生什么?

假设我们有两个哈希值 h A h_A hA 和 h B h_B hB,它们的高位差异极大,但低位完全一模一样:

  • h A h_A hA : 1111 0000 0000 0000 | 0000 0000 0000 0101
  • h B h_B hB : 0101 0101 0101 0101 | 0000 0000 0000 0101

未经扰动的下标计算:

n = 16 时,(16 - 1) 的二进制是 1111

  • A 的下标 : ...0101 & 1111 = 5
  • B 的下标 : ...0101 & 1111 = 5
  • 结果 : 发生严重碰撞!(高位的差异被完全忽略了)

3. 扰动函数介入:h ^ (h >>> 16)

扰动函数的目的就是:让高 16 位的特征"掉下来",混合到低 16 位中。

演示 A 的变换过程:

  1. 原值 h A h_A hA : 1111 0000 0000 0000 | 0000 0000 0000 0101
  2. 右移 16 位 : 0000 0000 0000 0000 | 1111 0000 0000 0000
  3. 异或运算:
text 复制代码
  1111 0000 0000 0000 | 0000 0000 0000 0101 (原值)
^ 0000 0000 0000 0000 | 1111 0000 0000 0000 (移位值)
-------------------------------------------
  1111 0000 0000 0000 | 1111 0000 0000 0101 (扰动结果)

最终 A 的低 4 位仍为 0101(十进制 5)。

演示 B 的变换过程:

  1. 原值 h B h_B hB : 0101 0101 0101 0101 | 0000 0000 0000 0101
  2. 右移 16 位 : 0000 0000 0000 0000 | 0101 0101 0101 0101
  3. 异或运算:
text 复制代码
  0101 0101 0101 0101 | 0000 0000 0000 0101 (原值)
^ 0000 0000 0000 0000 | 0101 0101 0101 0101 (移位值)
-------------------------------------------
  0101 0101 0101 0101 | 0101 0101 0101 0000 (扰动结果)

最终 B 的低 4 位变为了 0000(十进制 0)。


4. 最终对比:碰撞消失了!

处理方式 A 的下标 B 的下标 结果
直接取低位 5 5 碰撞!
扰动后再取 5 0 分流成功!

深度总结:

原本 A 和 B 的低 4 位完全相同。但通过右移 16 位,A 的高位特征 1111 和 B 的高位特征 0101 分别"揉"进了自己的低位。

即使你的数组很小,这种机制也强制让 32 位信息中的每一位都参与了最终的下标运算。这就是利用"40亿"种可能性来减少碰撞的精髓所在。


思维误区:其实原来哈希冲突的原因就是因为低位雷同,现在h&(h>>16)就是保证高16和低16位都是原值的高位信息,导致你h&(n-1)就是用不一样的高位去取模计算索引位置了?

你已经抓住了核心逻辑,但有一个小细节需要修正:不是"都是高位信息",而是"融合了高低位的信息"

你的理解中有一个很棒的洞察:"用不一样的高位去计算索引位置"。没错,就是通过"借位"的思想,让本来不参与运算的高位,变相地参与了运算。

为了确保你面试时描述得绝对精准,我们把这个逻辑最后梳理一下:

1. 修正一个小偏差:是"混合"而非"覆盖"

你提到的 h ^ (h >>> 16)

  • 高 16 位 :因为右移后高位补 0,h ^ 0 还是原值。所以高 16 位保持不变
  • 低 16 位 :变成了 原低16位 ^ 原高16位

所以,并不是说低位变成了纯高位信息,而是低位现在承载了全 32 位的"综合特征"

2. 为什么能减少冲突?(逻辑闭环)

  1. 原本的死穴n-1(比如 15)像是一个只看身份证最后 4 位的保安。只要最后 4 位一样,他就觉得是同一个人。
  2. 现在的解决办法:在过保安岗之前,我们先做一个动作------把身份证的前 16 位和后 16 位做一次异或。
  3. 结果:即使两个人的身份证后 4 位原本一样,但只要前 16 位有任何不同,异或后的"新后 4 位"大概率就不一样了。

3. 一个直观的对比

假设数组长度为 16(即 & 1111):

  • 没有扰动时

    • Key1: 0000...0001 & 1111 = 1
    • Key2: 1111...0001 & 1111 = 1冲突! 虽然 Key2 高位全是 1,但被保安无视了)
  • 有了扰动后

    • Key1: 低位还是接近 0001
    • Key2: 低位变成了 0001 ^ 1111 = 1110
    • Key2 计算索引:1110 & 1111 = 14
    • 冲突解除! 高位的 1111 成功自救,把 Key2 送到了 14 号位置。

相关推荐
苏渡苇21 天前
ConcurrentHashMap.computeIfAbsent():高并发下安全初始化的终极方案
java·安全·jdk·高并发·hashmap·concurrent
aygh1 个月前
互联网大厂Java面试场景:技术问答实录
mybatis·springboot·多线程·hashmap·java面试·arraylist·技术总结
weisian1511 个月前
Java并发编程--16-ConcurrentHashMap演进:从分段锁到CAS+synchronized
java·hashmap·分段锁·cas+同步·longaddr思想
予枫的编程笔记1 个月前
【面试专栏|Java并发编程】ConcurrentHashMap并发原理详解:JDK7 vs JDK8 核心对比
java·并发编程·hashmap·java面试·集合框架·jdk8·jdk7
没有bug.的程序员3 个月前
HashMap 源码深度剖析:红黑树转换机制与高并发性能陷阱
java·性能优化·并发编程·源码分析·红黑树·hashmap·技术深度
CodeAmaz3 个月前
ConcurrentHashMap(JDK 7/8)详细介绍
java·hashmap·线程安全map
CodeAmaz3 个月前
HashMap 面试全攻略
java·hashmap
予枫的编程笔记4 个月前
深度剖析 HashMap:从 JDK 1.7 死循环到 1.8 高低位映射优化
java·开发语言·散列表·hashmap
C雨后彩虹4 个月前
ConcurrentHashMap 扩容机制:高并发下的安全扩容实现
java·数据结构·哈希算法·集合·hashmap