HashMap 的 hash 方法是如何实现的?

一则或许对你有用的小广告

欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 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/

面试考察点

面试官提出这个问题,主要希望考察以下几个层面的理解和知识:

  1. 对 HashMap 底层数据结构的理解:考察你是否了解 HashMap 基于数组和链表/红黑树实现,以及 “哈希” 在这一结构中的核心作用。
  2. 对哈希冲突的认知与解决方案:考察你是否理解哈希函数的目标(均匀分布)以及如何通过设计减少冲突。
  3. 对位运算的掌握:考察你是否能看懂并解释源码中经典的位运算操作,这是理解 Java 集合框架源码的基础能力。
  4. 对 JDK 源码优化的关注与理解:特别是 JDK 7 与 JDK 8 在 hash 方法实现上的变化,这反映了候选人是否跟进技术演进和深度思考。

核心答案

HashMaphash 方法(在 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);
}

【深度解析】

原理/机制

这个设计的精髓在于 “扰动函数”

  1. 为何需要扰动?

    • key.hashCode() 返回的是一个 32 位的 int 型哈希值,范围从 -2^312^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 的哈希值低位相同而高位不同,将产生严重的冲突。
  2. 扰动如何工作?

    • (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 更友好。

最佳实践与注意事项

  1. 重写 hashCode()HashMaphash 方法始于 key.hashCode()。因此,如果你使用自定义对象作为 HashMap 的 Key,必须正确重写 hashCode()equals() 方法,确保逻辑相等的对象返回相同的哈希值。
  2. 容量为 2 的幂HashMap 要求容量为 2 的幂,这使得 (n-1) & hash 能均匀分布且高效。hash 方法的设计与这一前提紧密配合。
  3. 非加密哈希HashMap.hash() 是用于快速查找的扰动哈希,不具备加密安全性,也不保证绝对无冲突。

常见误区

  • 误区一:认为 HashMap.hash() 是为了生成最终的数组下标。不对,它只是生成一个“中间哈希值”,下标由 (n-1) & hash 决定。
  • 误区二:认为扰动次数越多越好。不对,JDK 8 的简化证明,在综合性能考量下,适度的扰动已足够。
  • 误区三:忽视 keynull 的情况。HashMap 允许一个 null key,其哈希值被固定为 0。

总结

HashMaphash 方法是一个精巧的扰动函数,它通过 hashCode() ^ (hashCode() >>> 16) 将键的高位特征融合到低位,旨在提升哈希值的分散性,从而与 (n-1) & hash 下标计算方式配合,有效减少哈希冲突,这是 HashMap 高效性的基石之一。