1、这里我来简单推导一下这个公式:假设 n=3i=0 - h = 31 * 0 + val0i=1 - h = 31 * (31 * 0 + val0) + val1i=2 - h = 31 * (31 * (31 * 0 + val0) + val1) + val2 h = 31*31*31*0 + 31*31*val0 + 31*val1 + val2 h = 31(n-1)*val0 + 31(n-2)*val1 + val2上面的公式包括公式的推导并不是本文的重点,大家了解了解即可。接下来来说说本文的重点,即选择31的理由。从网上的资料来看,一般有如下两个原因:第一,31是一个不大不小的质
2、数,是作为 hashCode 乘子的优选质数之一。另外一些相近的质数,比如37、41、43等等,也都是不错的选择。那么为啥偏偏选中了31呢?请看第二个原因。第二、31可以被 JVM 优化,31 * i = (i 5) - i。上面两个原因中,第一个需要解释一下,第二个比较简单,就不说了。下面我来解释第一个理由。一般在设计哈希算法时,会选择一个特殊的质数。至于为啥选择质数,我想应该是可以降低哈希算法的冲突率。至于原因,这个就要问数学家了,我几乎可以忽略的数学水平解释不了这个原因。上面说到,31是一个不大不小的质数,是优选乘子。那为啥同是质数的2和101(或者更大的质数)就不是优选乘子呢,分析如下
3、。这里先分析质数2。首先,假设n = 6,然后把质数2和 n 带入上面的计算公式。并仅计算公式中次数最高的那一项,结果是25 = 32,是不是很小。所以这里可以断定,当字符串长度不是很长时,用质数2做为乘子算出的哈希值,数值不会很大。也就是说,哈希值会分布在一个较小的数值区间内,分布性不佳,最终可能会导致冲突率上升。上面说了,质数2做为乘子会导致哈希值分布在一个较小区间内,那么如果用一个较大的大质数101会产生什么样的结果呢?根据上面的分析,我想大家应该可以猜出结果了。就是不用再担心哈希值会分布在一个小的区间内了,因为1015 = 10,510,100,501。但是要注意的是,这个计算结果太大
4、了。如果用 int 类型表示哈希值,结果会溢出,最终导致数值信息丢失。尽管数值信息丢失并不一定会导致冲突率上升,但是我们暂且先认为质数101(或者更大的质数)也不是很好的选择。最后,我们再来看看质数31的计算结果:315 = 28629151,结果值相对于32和10,510,100,501来说。是不是很nice,不大不小。上面用了比较简陋的数学手段证明了数字31是一个不大不小的质数,是作为 hashCode 乘子的优选质数之一。接下来我会用详细的实验来验证上面的结论,不过在验证前,我们先看看 Stack Overflow 上关于这个问题的讨论,Why does Javas hashCode()
5、 in String use 31 as a multiplier?。其中排名第一的答案引用了Effective Java中的一段话,这里也引用一下:The value 31 was chosen because it is an odd prime. If it were even and the multiplication overflowed, information would be lost, as multiplication by 2 is equivalent to shifting. The advantage of using a prime is less clear,
6、 but it is traditional. A nice property of 31 is that the multiplication can be replaced by a shift and a subtraction for better performance: 31 * i = (i 5) - i. Modern VMs do this sort of optimization automatically.简单翻译一下:选择数字31是因为它是一个奇质数,如果选择一个偶数会在乘法运算中产生溢出,导致数值信息丢失,因为乘二相当于移位运算。选择质数的优势并不是特别的明显,但这是
7、一个传统。同时,数字31有一个很好的特性,即乘法运算可以被移位和减法运算取代,来获取更好的性能:31 * i = (i 5) - i,现代的 Java 虚拟机可以自动的完成这个优化。排名第二的答案设这样说的:As Goodrich and Tamassia point out, If you take over 50,000 English words (formed as the union of the word lists provided in two variants of Unix), using the constants 31, 33, 37, 39, and 41 will
8、produce less than 7 collisions in each case. Knowing this, it should come as no surprise that many Java implementations choose one of these constants.这段话也翻译一下:正如 Goodrich 和 Tamassia 指出的那样,如果你对超过 50,000 个英文单词(由两个不同版本的 Unix 字典合并而成)进行 hash code 运算,并使用常数 31, 33, 37, 39 和 41 作为乘子,每个常数算出的哈希值冲突数都小于7个,所以在上面
9、几个常数中,常数 31 被 Java 实现所选用也就不足为奇了。上面的两个答案完美的解释了 Java 源码中选用数字 31 的原因。接下来,我将针对第二个答案就行验证,请大家继续往下看。3. 实验及数据可视化本节,我将使用不同的数字作为乘子,对超过23万个英文单词进行哈希运算,并计算哈希算法的冲突率。同时,我也将针对不同乘子算出的哈希值分布情况进行可视化处理,让大家可以直观的看到数据分布情况。本次实验所使用的数据是 Unix/Linux 平台中的英文字典文件,文件路径为/usr/share/dict/words。3.1 哈希值冲突率计算计算哈希算法冲突率并不难,比如可以一次性将所有单词的 ha
10、sh code 算出,并放入 Set 中去除重复值。之后拿单词数减去 set.size() 即可得出冲突数,有了冲突数,冲突率就可以算出来了。当然,如果使用 JDK8 提供的流式计算 API,则可更方便算出,代码片段如下:public static Integer hashCode(String str, Integer multiplier) int hash = 0; str.length(); hash = multiplier * hash + str.charAt(i); return hash; /* * 计算 hash code 冲突率,顺便分析一下 hash code 最大值和
11、最小值,并输出 * param multiplier * param hashs */public static void calculateConflictRate(Integer multiplier, List hashs) Comparator x y ? 1 : (x 数值对,这样就可以绘图了。分区代码如下: /* * 将整个哈希空间等分成64份,统计每个空间内的哈希值数量public static Map partition(List / step = 232 / 64 = 226 final int step = 67108864; List nums = new ArrayLi
12、st(); Map statistics = new LinkedHashMap= min & x x + y).get(); assert hashNum = hashs.size(); return statistics;本文中的哈希值是用整形表示的,整形的数值区间是-2147483648, 2147483647,区间大小为232。所以这里可以将区间等分成64个子区间,每个自子区间大小为226。详细的分区对照表如下:分区编号分区下限分区上限-2147483648-208037478432671088641-2013265920331342177282-1946157056342013265
13、923-1879048192352684354564-1811939328363355443205-1744830464374026531846-1677721600384697620487-1610612736395368709128-1543503872406039797769-14763950084167108864010-14092861444273819750411-13421772804380530636812-12750684164487241523213-12079595524593952409614-114085068846100663296015-1073741824471
14、07374182416-100663296048114085068817-93952409649120795955218-87241523250127506841619-80530636851134217728020-73819750452140928614421-67108864053147639500822-60397977654154350387223-53687091255161061273624-46976204856167772160025-40265318457174483046426-33554432058181193932827-26843545659187904819228
15、-20132659260194615705629-13421772861201326592030-6710886462208037478431632147483648接下来,让我们对照上面的分区表,对数字2、3、17、31、101的散点曲线图进行简单的分析。先从数字2开始,数字2对于的散点曲线图如下:上面的图还是很一幕了然的,乘子2算出的哈希值几乎全部落在第32分区,也就是0, 67108864)数值区间内,落在其他区间内的哈希值数量几乎可以忽略不计。这也就不难解释为什么数字2作为乘子时,算出哈希值的冲突率如此之高的原因了。所以这样的哈希算法要它有何用啊,拖出去斩了吧。接下来看看数字3作为乘子
16、时的表现:3作为乘子时,算出的哈希值分布情况和2很像,只不过稍微好了那么一点点。从图中可以看出绝大部分的哈希值最终都落在了第32分区里,哈希值的分布性很差。这个也没啥用,拖出去枪毙5分钟吧。在看看数字17的情况怎么样:数字17作为乘子时的表现,明显比上面两个数字好点了。虽然哈希值在第32分区和第34分区有一定的聚集,但是相比较上面2和3,情况明显好好了很多。除此之外,17作为乘子算出的哈希值在其他区也均有分布,且较为均匀,还算是一个不错的乘子吧。接下来来看看我们本文的主角31了,31作为乘子算出的哈希值在第33分区有一定的小聚集。不过相比于数字17,主角31的表现又好了一些。首先是哈希值的聚集
17、程度没有17那么严重,其次哈希值在其他区分布的情况也要好于17。总之,选31,准没错啊。最后再来看看大质数101的表现,不难看出,质数101作为乘子时,算出的哈希值分布情况要好于主角31,有点喧宾夺主的意思。不过不可否认的是,质数101的作为乘子时,哈希值的分布性确实更加均匀。所以如果不在意质数101容易导致数据信息丢失问题,或许其是一个更好的选择。4.写在最后经过上面的分析与实践,我想大家应该明白了 String hashCode 方法中选择使用数字31作为乘子的原因了。本文本质是一篇简单的科普文而已,并没有银弹。如果大家读完后觉得又涨知识了,那这篇文章的目的就达到了。最后,本篇文章的配图画的还是很辛苦的,所以如果大家觉得文章不错,不妨就给个赞吧,就当是对我的鼓励了。另外,如果文章中有不妥或者错误的地方,也欢迎指出来。如果能不吝赐教,那就更好了。最后祝大家生活愉快,再见。
copyright@ 2008-2022 冰豆网网站版权所有
经营许可证编号:鄂ICP备2022015515号-1