HashMap 的 hash 方法是如何实现的?
2026年01月23日
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
面试考察点
面试官提出这个问题,主要希望考察以下几个层面的理解和知识:
- 对 HashMap 底层数据结构的理解:考察你是否了解 HashMap 基于数组和链表/红黑树实现,以及 “哈希” 在这一结构中的核心作用。
- 对哈希冲突的认知与解决方案:考察你是否理解哈希函数的目标(均匀分布)以及如何通过设计减少冲突。
- 对位运算的掌握:考察你是否能看懂并解释源码中经典的位运算操作,这是理解 Java 集合框架源码的基础能力。
- 对 JDK 源码优化的关注与理解:特别是 JDK 7 与 JDK 8 在
hash方法实现上的变化,这反映了候选人是否跟进技术演进和深度思考。
核心答案
HashMap 的 hash 方法(在 JDK 8 中实际是 HashMap.hash(Object key) 静态方法,内部由 putVal 等方法调用)核心目标是:将键(key)的 hashCode() 值进行二次处理(扰动),目的是让高位比特也能参与后续的数组下标计算,从而降低哈希冲突的概率。
在 JDK 8 中,其实现非常简洁,只有一行核心代码:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
【深度解析】
原理/机制
这个设计的精髓在于 “扰动函数”。
-
为何需要扰动?
key.hashCode()返回的是一个 32 位的int型哈希值,范围从-2^31到2^31-1。而HashMap底层数组的初始容量(如 16)通常很小。- 在计算数组下标时,会执行
(n - 1) & hash操作(n是数组长度)。当n是 2 的幂时,(n - 1) & hash等价于hash % n,但位运算效率更高。 - 如果直接用原始的
hashCode和(n-1)做&操作,由于n-1的二进制高位基本都是 0(例如15的二进制是0000...00001111),这会导致只有哈希值的低几位真正参与了运算,而高位的变化完全被忽略。如果多个 key 的哈希值低位相同而高位不同,将产生严重的冲突。
-
扰动如何工作?
(h = key.hashCode()) ^ (h >>> 16)这段代码将哈希值h无符号右移 16 位,相当于将高 16 位移动到了低 16 位。- 然后,将原哈希值
h与移位后的值进行异或 (^) 操作。 - 异或操作的特性是:相同为 0,不同为 1。这个操作将原哈希值的高 16 位信息,“混合” 到了低 16 位中。
- 这样,在后续执行
(n - 1) & hash时,实际参与运算的低位就同时包含了原始哈希值高位和低位的信息,显著增加了哈希的随机性,减少了冲突。
代码示例
我们可以模拟一个过程:
// 假设某个 key.hashCode() 的值 h 为:
// 高16位: 1111111111111111
// 低16位: 0000000000000000
// 二进制: 1111111111111111 0000000000000000
int h = 0xFFFF0000; // 十进制:4294901760
// JDK 8 的 hash 方法计算过程
int shifted = h >>> 16; // 无符号右移16位: 0000000000000000 1111111111111111 (即0x0000FFFF)
int finalHash = h ^ shifted; // 异或操作
// h: 1111111111111111 0000000000000000
// shifted:0000000000000000 1111111111111111
// ^ 结果: 1111111111111111 1111111111111111 (即0xFFFFFFFF)
// 假设 table 长度 n=16 (n-1=15,二进制 0000...00001111)
int index = (n - 1) & finalHash; // 即 15 & 0xFFFFFFFF
// 15: 0000...000000000000000000001111
// finalHash: ...111111111111111111111111
// & 结果: 0000...000000000000000000001111 (即15)
// 可以看到,最终下标计算用到了混合后的低4位信息(全是1)
对比分析:JDK 7 与 JDK 8
- JDK 7 的实现更复杂,进行了 4 次扰动:
h ^= k.hashCode(); h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); - JDK 8 将其简化为 1 次异或和 1 次移位。这是因为经过评估,在引入了红黑树优化长链表性能后,即使哈希冲突稍有增加,其性能影响也在可控范围内。简化后的算法在计算效率和分布均匀性之间取得了更好的平衡,且对现代 CPU 更友好。
最佳实践与注意事项
- 重写
hashCode():HashMap的hash方法始于key.hashCode()。因此,如果你使用自定义对象作为HashMap的 Key,必须正确重写hashCode()和equals()方法,确保逻辑相等的对象返回相同的哈希值。 - 容量为 2 的幂:
HashMap要求容量为 2 的幂,这使得(n-1) & hash能均匀分布且高效。hash方法的设计与这一前提紧密配合。 - 非加密哈希:
HashMap.hash()是用于快速查找的扰动哈希,不具备加密安全性,也不保证绝对无冲突。
常见误区
- 误区一:认为
HashMap.hash()是为了生成最终的数组下标。不对,它只是生成一个“中间哈希值”,下标由(n-1) & hash决定。 - 误区二:认为扰动次数越多越好。不对,JDK 8 的简化证明,在综合性能考量下,适度的扰动已足够。
- 误区三:忽视
key为null的情况。HashMap允许一个nullkey,其哈希值被固定为 0。
总结
HashMap 的 hash 方法是一个精巧的扰动函数,它通过 hashCode() ^ (hashCode() >>> 16) 将键的高位特征融合到低位,旨在提升哈希值的分散性,从而与 (n-1) & hash 下标计算方式配合,有效减少哈希冲突,这是 HashMap 高效性的基石之一。