根据Java文档,String对象的哈希代码是这样计算的:

S [0]*31^(n-1) + S [1]*31^(n-2) +…+ s (n - 1) 使用int算术,其中s[i]是 字符串的第i个字符,n是的长度 字符串,^表示取幂。

为什么用31作为乘数?

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


当前回答

对哈希函数的一个很大的期望是,它们的结果的均匀随机性在哈希(x) % N这样的操作中仍然存在,其中N是一个任意数字(在许多情况下,是2的幂),一个原因是这样的操作通常用于哈希表中确定槽。在计算散列时使用质数乘数会降低乘数和N个共享除数的概率,这将使操作的结果不那么均匀随机。

还有人指出了一个很好的性质,即乘31可以由一个乘法和一个减法来完成。我只是想指出这样的质数有一个数学术语:梅森质数

所有梅森质数都比2的幂小1所以我们可以写成

p = 2^n - 1

用x乘以p:

x * p = x * (2^n - 1) = x * 2^n - x = (x << n) - x

在许多机器上,移位(SAL/SHL)和减法(SUB)通常比乘法(MUL)快。参见Agner Fog的指令表

这就是为什么GCC似乎通过将梅森质数的乘法替换为移位和置换来优化梅森质数的乘法,见这里。

然而,在我看来,如此小的质数对于哈希函数来说是一个糟糕的选择。对于一个相对较好的哈希函数,你会期望在哈希的较高位具有随机性。然而,使用Java哈希函数,对于较短的字符串,在较高的位上几乎没有随机性(在较低的位上仍然具有高度可疑的随机性)。这使得构建高效哈希表变得更加困难。看看这个你用Java哈希函数做不到的小技巧。

一些回答提到,他们认为31适合一个字节是好的。这实际上是无用的,因为:

(1)我们执行移位而不是乘法,因此乘数的大小无关紧要。

(2)据我所知,没有特定的x86指令来将一个8字节的值与一个1字节的值相乘,所以即使是相乘,你也需要将“31”转换为8字节的值。看这里,你将整个64位寄存器相乘。

(127实际上是一个字节所能容纳的最大梅森素数。)

较小的值是否会增加中下位的随机性?也许吧,但这似乎也大大增加了可能的碰撞:)。

人们可以列出许多不同的问题,但它们通常可以归结为两个未能很好地实现的核心原则:混乱和扩散

但是速度快吗?可能吧,因为它没什么用。然而,如果性能真的是这里的重点,那么每个循环一个字符是相当低效的。对于更长的字符串,为什么不每次循环迭代4个字符(8字节),就像这样?好吧,这将很难与当前定义的哈希,你需要单独乘以每个字符(请告诉我如果有一点hack来解决这个问题:D)。

其他回答

我不确定,但我猜他们测试了一些质数样本,发现31在一些可能的字符串样本中给出了最好的分布。

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

The table below summarizes the performance of the various hash functions described above, for three data sets: 1) All of the words and phrases with entries in Merriam-Webster's 2nd Int'l Unabridged Dictionary (311,141 strings, avg length 10 chars). 2) All of the strings in /bin/, /usr/bin/, /usr/lib/, /usr/ucb/ and /usr/openwin/bin/* (66,304 strings, avg length 21 characters). 3) A list of URLs gathered by a web-crawler that ran for several hours last night (28,372 strings, avg length 49 characters). The performance metric shown in the table is the "average chain size" over all elements in the hash table (i.e., the expected value of the number of key compares to look up an element). 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 Looking at this table, it's clear that all of the functions except for the current Java function and the two broken versions of Weinberger's function offer excellent, nearly indistinguishable performance. I strongly conjecture that this performance is essentially the "theoretical ideal", which is what you'd get if you used a true random number generator in place of a hash function. I'd rule out the WAIS function as its specification contains pages of random numbers, and its performance is no better than any of the far simpler functions. Any of the remaining six functions seem like excellent choices, but we have to pick one. I suppose I'd rule out Vo's variant and Weinberger's function because of their added complexity, albeit minor. Of the remaining four, I'd probably select P(31), as it's the cheapest to calculate on a RISC machine (because 31 is the difference of two powers of two). P(33) is similarly cheap to calculate, but it's performance is marginally worse, and 33 is composite, which makes me a bit nervous. Josh

通过相乘,位向左移位。这使用了更多哈希码的可用空间,减少了冲突。

通过不使用2的幂,低阶,最右边的位也被填充,与进入散列的下一段数据混合。

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

尼尔·科菲解释了为什么在熨平偏差下使用31。

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

Bloch并没有深入研究这个问题,但我总是听到/相信这是基本的代数。哈希可以归结为乘法和模运算,这意味着如果可以的话,永远不要使用带公因式的数字。换句话说,相对素数提供了答案的均匀分布。

使用哈希的数字通常是:

你放入的数据类型的模量 (2^32或2^64) 哈希表中桶数的模数(变化。在java中,以前是质数,现在是2^n) 在混合函数中乘以或平移一个神奇的数字 输入值

实际上,您只能控制其中的几个值,因此需要多加注意。