为什么Java中String的hashCode()方法使用31作为乘数?

578
根据Java文档,计算字符串对象的哈希码的方法如下:

哈希码是针对String对象计算的。

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

使用int算术,在此情况下,其中s [i]是字符串的第i个字符,n是字符串的长度,^表示指数。

为什么使用31作为乘数?

我知道乘数应该是一个相对较大的质数。那么为什么不选用29、37或甚至是97呢?


1
还可以参考https://dev59.com/SHI-5IYBdhLWcg3weoPR - 如果你要编写自己的hashCode函数,我认为31是一个不好的选择。 - Hans-Peter Störr
16
如果是29、37,甚至是97的话,你会问:“为什么不是31呢?” - user207421
3
重要的是了解选择某个数字的原因,除非该数字是黑魔法技巧的结果。 - Dushyant Sabharwal
这里有一篇关于此的博客文章,作者是@peter-lawrey:https://vanilla-java.github.io/2018/08/12/Why-do-I-think-Stringhash-Code-is-poor.html 和 https://vanilla-java.github.io/2018/08/15/Looking-at-randomness-and-performance-for-hash-codes.html。 - Christophe Roussy
1
我的观点是,它本可以是29、37、97、41或许多其他值,而这并没有产生太大的实际差异。我们在1976年使用了37。 - user207421
13个回答

475
根据Joshua Bloch的《Effective Java, Second Edition》(这是一本非常值得推荐的书,我在Stack Overflow上不断看到它的提及而购买了它):
“选择31作为哈希码计算中的乘数是因为它是一个奇素数。如果选用偶数,当乘法溢出时,信息就会丢失,因为乘以2等同于移位。使用质数的优点不太明显,但这是传统的做法。31的一个好处是可以通过移位和减法来代替乘法,从而提高性能:31 * i == (i << 5) - i。现代虚拟机会自动进行此类优化。”
(摘自第3章第9条:“总是在覆盖equals时覆盖hashCode”,第48页)

407
所有的质数都是奇数,除了2。就这样说。 - Kip
48
我认为Bloch的意思并不是说它被选择,仅仅因为它是一个奇数质数,而是因为它既是奇数又是质数(并且因为它可以很容易地被优化为移位/减法)。 - matt b
58
31被选择是因为它能提供最好的分布效果,而不是因为它是一个奇素数。请查看http://computinglife.wordpress.com/2008/11/20/why-do-hash-functions-use-prime-numbers/。 - computinglife
71
我认为选择31相当不幸。确实,它可能节省一些旧机器的CPU周期,但是在短ASCII字符串(如“@和#!”或“Ca和DB”)上已经存在哈希冲突。如果你选择1327144003,或者至少选择524287,也允许位移操作:524287 * i == i << 19 - i,那么这种情况就不会发生。 - Hans-Peter Störr
15
请看我的回答:@Jason,请查看我的回答https://dev59.com/SHI-5IYBdhLWcg3weoPR#2816747 。我的意思是,如果使用一个更大的质数,会使冲突少得多,并且现在不会有任何损失。如果使用常见的非 ASCII 字符的非英语语言,则问题会更加严重。而当程序员编写自己的 hashCode 函数时,31 作为一个糟糕的例子。 - Hans-Peter Störr
显示剩余10条评论

97
使用常数31、33、37、39和41进行计算,Goodrich和Tamassia从超过50,000个英语单词(由Unix的两个变体提供的单词列表并集组成)中得出结论,每种情况下产生少于7个碰撞。这可能是许多Java实现选择这些常量的原因。
请参见数据结构与算法Java语言描述第9.2节散列表(第522页)。

8
请注意,如果您使用任何国际字符集并包含ASCII范围之外的常见字符,则可能会导致更多的碰撞。至少,我已经为31和德语进行了检查。因此,我认为选择31是错误的。 - Hans-Peter Störr

61

在(大多数)旧处理器上,乘以31可能相对便宜。例如,在ARM上,它只是一条指令:

RSB       r1, r0, r0, ASL #5    ; r1 := - r0 + (r0<<5)

大多数其他处理器需要单独的移位和减法指令,但是如果您的乘法器很慢,这仍然是一种胜利。现代处理器往往具有快速的乘法器,因此只要32位进入正确的位置,就不会有太大的区别。

它不是一个很好的哈希算法,但足够好并且比1.0代码(以及1.0规范)好得多。


7
有趣的是,与例如92821相比,在我的桌面电脑上,使用31进行乘法实际上会稍微慢一些。我猜编译器也试图将其优化为移位和加法。 :-) - Hans-Peter Störr
1
我认为我从未使用过的ARM处理器在范围+/-255内的所有值都同样快。使用2的幂减1会不幸地导致两个值的匹配更改将哈希码更改为2的幂次方。-31的值可能更好,而像-83(64 + 16 + 2 + 1)这样的值可能更好(更好地混合位)。 - supercat
@supercat 我不认同使用负号的做法。看起来你会回到零的方向。String.hashCode 的诞生早于 StrongARM 处理器,据我所知,它引入了一个 8 位乘法器,并可能将组合算术/逻辑与移位操作增加到了两个周期。 - Tom Hawtin - tackline
1
@TomHawtin-tackline:使用31,四个值的哈希将是29791a + 961b + 31c + d;使用-31,它将是-29791a + 961b - 31c + d。如果这四个项目是独立的,我认为差异不会很大,但是如果相邻项目成对匹配,则生成的哈希码将是所有未配对项目的贡献,加上32的某些倍数(来自配对项目)。对于字符串,可能并不重要,但是如果编写用于哈希聚合的通用方法,则相邻项目匹配的情况将不成比例地普遍。 - supercat
3
有趣的事实是,根据规范,Map.Entry 的哈希码已被固定为 key.hashCode() ^ value.hashCode(),即使它甚至不是一个无序对,因为 keyvalue 具有完全不同的含义。是的,这意味着 Map.of(42, 42).hashCode()Map.of("foo", "foo", "bar", "bar").hashCode() 等的哈希码可以可预测地为零。因此,不要将映射用作其他映射的键... - Holger
显示剩余7条评论

43

通过乘法,位被向左移动,这使用哈希码的可用空间更多,减少碰撞。

不使用2的幂次方时,低阶的右侧位也会被填充,以与进入哈希的下一段数据混合。

表达式n * 31等同于(n << 5) - n


38
您可以在 http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4045622 的“评论”中阅读 Bloch 的原始论据。他研究了不同哈希函数的性能,关注点是哈希表中生成的“平均链大小”。P(31) 是当时常见的函数之一,他在 K&R 的书中找到了它(但连 Kernighan 和 Ritchie 也记不清它来自何处)。最后,他基本上不得不选择一个,所以他选择了 P(31),因为它似乎表现足够好。即使 P(33) 实际上并不更差,并且乘以 33 同样快速计算(只需移位 5 次和加法),他还是选择了 31,因为 33 不是质数:

剩下的四个函数中,我可能会选择 P(31),因为在 RISC 机器上它是最便宜的计算方式(因为 31 是两个二次幂的差)。P(33) 的计算成本也类似低廉,但性能略逊,而且 33 是合成数,这让我有些不安。

所以这个推理不像许多回答中所暗示的那样合理。但我们都可以在决策后提出合理的理由(甚至 Bloch 也可能会这样做)。


25

实际上,37也可以很好地运作!z:= 37 * x 可以计算为y := x + 8 * x; z := x + 4 * y。这两步对应一个LEA x86指令,因此非常快速。

事实上,同样速度的情况下,使用更大的质数73进行乘法也可以通过设置y := x + 8 * x; z := x + 8 * y来完成。

使用73或37(而不是31)可能会更好,因为它会导致更密集的代码:两个LEA指令只需要6个字节,而使用31进行乘法则需要7个字节的移位加减运算。可能的一个注意点是,这里使用的3参数LEA指令在英特尔Sandy Bridge架构上变得更慢了,延迟增加了3个周期。

此外,73是Sheldon Cooper最喜欢的数字。


11
实际上,这是ALGOL语法,并且在伪代码中经常使用。 - ApproachingDarknessFish
4
在ARM汇编中,可以通过一条指令完成乘以31的操作。 - phuclv
5
在伪代码中,":=" 表示赋值操作。 - phuclv
在《编程实践》(1999)中,我们可以阅读到有关早期Java的内容(第57页):“...问题是通过将哈希替换为等效于我们展示的哈希(乘数为37)来解决的...” - miku

19

Neil Coffey 解释 为什么在消除偏差的情况下使用31。

基本上,使用31可以为哈希函数提供更均匀的位集概率分布。


17

JDK-4045622中,Joshua Bloch描述了为什么选择特定(新)String.hashCode()实现的原因。

下表总结了上述各种哈希函数的性能,针对三个数据集:

1)Merriam-Webster第二国际无缩写词典中所有单词和短语的条目(311,141个字符串,平均长度为10个字符)。

2)/bin/,/usr/bin/,/usr/lib/,/usr/ucb/和/usr/openwin/bin/* 中所有字符串(66,304个字符串,平均长度为21个字符)。

3)由网络爬虫在昨晚运行了几个小时后收集到的URL列表(28,372个字符串,平均长度为49个字符)。

表中显示的性能指标是哈希表中所有元素的“平均链大小”(即期望值,用于查找元素需要的键比较次数)。

                          Webster's   Code Strings    URLs
                          ---------   ------------    ----
Current Java Fn.          1.2509      1.2738          13.2560
P(37)    [Java]           1.2508      1.2481          1.2454
P(65599) [Aho et al]      1.2490      1.2510          1.2450
P(31)    [K+R]            1.2500      1.2488          1.2425
P(33)    [Torek]          1.2500      1.2500          1.2453
Vo's Fn                   1.2487      1.2471          1.2462
WAIS Fn                   1.2497      1.2519          1.2452
Weinberger's Fn(MatPak)   6.5169      7.2142          30.6864
Weinberger's Fn(24)       1.3222      1.2791          1.9732
Weinberger's Fn(28)       1.2530      1.2506          1.2439

从这张表格来看,除了当前的Java函数和Weinberger的两个破损版本之外,所有的函数表现都非常出色,几乎无法区分。我强烈推测,这种性能基本上是"理论上的理想状态",如果您使用真正的随机数生成器替代哈希函数,那么就可以达到这种性能。

我会排除WAIS函数,因为其规范包含大量随机数,并且其性能并不比任何更简单的函数更好。剩下的六个函数中的任意一个都似乎是很好的选择,但我们必须选择一个。我想我会排除Vo的变体和Weinberger的函数,因为它们增加了复杂度,尽管是轻微的。在剩下的四个函数中,我可能会选择P(31),因为它在RISC机器上计算最便宜(因为31是两个二次幂之差)。P(33)的计算成本也相似,但其性能略差,并且33是合数,这让我有点担心。

Josh


6

Java字符串hashCode()方法和31

这是因为数字31有一个好的特性——它的乘法可以被位移操作替换,而位移操作比标准的乘法更快:

31 * i == (i << 5) - i

5
Bloch没有详细解释这一点,但我一直听说/相信的理由是这是基本的代数运算。哈希表归结为乘法和模运算,这意味着您永远不想使用具有公共因子的数字,如果可以避免的话。换句话说,互质的数字提供了均匀分布的答案。
组成哈希值的数字通常是:
  • 将其放入数据类型的模数 (2^32或2^64)
  • 哈希表中桶计数的模数(变化。在java中曾经是素数,现在是2^n)
  • 在混合函数中乘以或移位魔术数字
  • 输入值
实际上,您只能控制其中的几个值,所以需要更加小心。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接