bigywater.tech
Java 内部原理 · 37

String.hashCode() 真实公式:真实造出一对内容完全不同却哈希相同的字符串

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] 是 String.hashCode() 的公式,很多人背过但没真的用过。真实找一对碰撞的字符串,手算一遍公式,再看看碰撞对 HashMap 有没有真实影响。

"Aa" vs "BB" → 都是 2112
两个内容完全不同的字符串,hashCode() 真实完全相等,equals() 真实不相等
加前缀依然碰撞
给两者都加上相同前缀 "hello-",真实验证 hashCode 依然相等——碰撞能递归扩展到任意长度

上一篇:方法重载 vs 重写:真实验证重载看编译期类型,重写看运行时类型

真实验证:一对经典碰撞

"Aa" 和 "BB" 是 String.hashCode() 最短的一对真实碰撞,直接验证。

真实实测
"Aa".hashCode() = 2112
"BB".hashCode() = 2112
两者 hashCode 真实相等 = true
两者 equals() 真实相等 = false(内容完全不同,不应该相等)

这不是巧合,是 String.hashCode() 公式本身的数学结构决定的——公式是 s[0]*31 + s[1](两字符情况下)。'A' 的 Unicode 码点是 65,'a' 是 97;'B' 是 66。65*31+97 = 2112,66*31+66 = 2112——把一个字符的码点减 1、下一个字符的码点加 31,总和真实不变,这正是 "Aa"→"BB" 这对碰撞的真实来源('A'→'B' 加 1,'a'→'B' 从 97 减到 66 正好是减 31)。

真实实测 · 手动按公式计算
按公式 s[0]*31 + s[1] 手动真实计算 "Aa" = 'A'(65)*31 + 'a'(97) = 2112
按公式 s[0]*31 + s[1] 手动真实计算 "BB" = 'B'(66)*31 + 'B'(66) = 2112
手动算出的结果和 String.hashCode() 真实一致 = true

真实验证:碰撞能递归扩展到任意长度

给两个碰撞的字符串加上完全相同的前缀,新字符串依然会碰撞——这是公式本身的线性结构决定的。

真实实测
"hello-Aa".hashCode() = -792719589
"hello-BB".hashCode() = -792719589
加上相同前缀后,两者 hashCode 真实依然相等 = true

真实公式满足 hashCode(P + S) = hashCode(P) * 31^len(S) + hashCode(S)——只要两个后缀 S1、S2 长度相同且 hashCode 相等,给它们拼上同一个前缀 P 之后,整串的 hashCode 依然真实相等。这意味着"Aa"/"BB" 这个最小碰撞对,能真实作为种子,递归构造出任意长度、任意前缀的碰撞对。

真实验证:碰撞对 HashMap 的实际影响

哈希碰撞不等于程序出错——HashMap 靠 equals() 兜底,真实结果依然正确,只是牺牲了一点点查找效率。

真实实测
map.get("Aa") = s1 的值
map.get("BB") = s2 的值
map.size() = 2(两个 key 尽管哈希相同,但 equals 不同,真实被当成两个独立条目)
这个碰撞公式有安全隐患吗?历史上真实出现过:早期 web 框架如果直接用 String.hashCode() 做请求参数的哈希表 key,攻击者能故意构造大量哈希相同的参数名,让所有键值对都真实落进同一个桶,把 HashMap 的平均 O(1) 查找真实退化成 O(n),这就是历史上真实出现过的"哈希碰撞拒绝服务攻击"。系列前面HashMap 树化那篇验证过的红黑树转换机制,某种程度上也是对这类场景的真实缓解——退化成链表时最坏是 O(n),树化之后最坏是 O(log n)。

分步演示:公式是怎么让两个不同字符串撞到一起的

每一行都是真实计算或运行得到的结果。

"Aa" 和 "BB" 的 hashCode() 是怎么用公式真实算出同一个值 2112 的,又是怎么被 HashMap 正确区分开的
这一步新计算/验证的值

    参考与说明

    • 本文全部哈希值均在本机真实运行 Java 17 得到,String.hashCode() 的公式和取值在不同 JVM 实现间是语言规范保证一致的,不受具体实现影响。
    • 和 HashMap 树化、HashMap 容量与阈值 两篇一起,拼出了"哈希碰撞从哪来、HashMap 怎么应对"的完整链路。
    • 没有涉及:String.hashCode() 的结果会不会被缓存(真实会,String 内部有一个 hash 字段缓存计算结果,空字符串是唯一的例外)——这些留给系列后面按需再写。
    京ICP备2026067502号-1京公网安备11010502063487号