s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] 是 String.hashCode() 的公式,很多人背过但没真的用过。真实找一对碰撞的字符串,手算一遍公式,再看看碰撞对 HashMap 有没有真实影响。
上一篇:方法重载 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 靠 equals() 兜底,真实结果依然正确,只是牺牲了一点点查找效率。
map.get("Aa") = s1 的值
map.get("BB") = s2 的值
map.size() = 2(两个 key 尽管哈希相同,但 equals 不同,真实被当成两个独立条目)
每一行都是真实计算或运行得到的结果。