某天,我在写代码的时候,无心中点开了 String hashCode 方法。而后大体看了一下 hashCode 的实现,发现并非很复杂。可是我从源码中发现了一个奇怪的数字,也就是本文的主角31。这个数字竟然不是用常量声明的,因此无法从字面意思上推断这个数字的用途。后来带着疑问和好奇心,到网上去找资料查询一下。在看完资料后,默默的感叹了一句,原来是这样啊。那么究竟是哪样呢?在接下来章节里,请你们带着好奇心和我揭开数字31的用途之谜。java
在详细说明 String hashCode 方法选择数字31的做为乘子的缘由以前,咱们先来看看 String hashCode 方法是怎样实现的,以下:算法
public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; }
上面的代码就是 String hashCode 方法的实现,是否是很简单。实际上 hashCode 方法核心的计算逻辑只有三行,也就是代码中的 for 循环。咱们能够由上面的 for 循环推导出一个计算公式,hashCode 方法注释中已经给出。以下:数组
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
这里说明一下,上面的 s 数组即源码中的 val 数组,是 String 内部维护的一个 char 类型数组。这里我来简单推导一下这个公式:less
假设 n=3 i=0 -> h = 31 * 0 + val[0] i=1 -> h = 31 * (31 * 0 + val[0]) + val[1] i=2 -> h = 31 * (31 * (31 * 0 + val[0]) + val[1]) + val[2] h = 31*31*31*0 + 31*31*val[0] + 31*val[1] + val[2] h = 31^(n-1)*val[0] + 31^(n-2)*val[1] + val[2]
上面的公式包括公式的推导并非本文的重点,你们了解了解便可。接下来来讲说本文的重点,即选择31的理由。从网上的资料来看,通常有以下两个缘由:ide
第一,31是一个不大不小的质数,是做为 hashCode 乘子的优选质数之一。另一些相近的质数,好比3七、4一、43等等,也都是不错的选择。那么为啥恰恰选中了31呢?请看第二个缘由。工具
第2、31能够被 JVM 优化,31 * i = (i << 5) - i
。性能
上面两个缘由中,第一个须要解释一下,第二个比较简单,就不说了。下面我来解释第一个理由。通常在设计哈希算法时,会选择一个特殊的质数。至于为啥选择质数,我想应该是能够下降哈希算法的冲突率。至于缘由,这个就要问数学家了,我几乎能够忽略的数学水平解释不了这个缘由。上面说到,31是一个不大不小的质数,是优选乘子。那为啥同是质数的2和101(或者更大的质数)就不是优选乘子呢,分析以下。优化
这里先分析质数2。首先,假设 n = 6
,而后把质数2和 n 带入上面的计算公式。并仅计算公式中次数最高的那一项,结果是2^5 = 32
,是否是很小。因此这里能够判定,当字符串长度不是很长时,用质数2作为乘子算出的哈希值,数值不会很大。也就是说,哈希值会分布在一个较小的数值区间内,分布性不佳,最终可能会致使冲突率上升。ui
上面说了,质数2作为乘子会致使哈希值分布在一个较小区间内,那么若是用一个较大的大质数101会产生什么样的结果呢?根据上面的分析,我想你们应该能够猜出结果了。就是不用再担忧哈希值会分布在一个小的区间内了,由于101^5 = 10,510,100,501
。可是要注意的是,这个计算结果太大了。若是用 int 类型表示哈希值,结果会溢出,最终致使数值信息丢失。尽管数值信息丢失并不必定会致使冲突率上升,可是咱们暂且先认为质数101(或者更大的质数)也不是很好的选择。最后,咱们再来看看质数31的计算结果: 31^5 = 28629151
,结果值相对于32
和10,510,100,501
来讲。是否是很nice,不大不小。this
上面用了比较简陋的数学手段证实了数字31是一个不大不小的质数,是做为 hashCode 乘子的优选质数之一。接下来我会用详细的实验来验证上面的结论,不过在验证前,咱们先看看 Stack Overflow 上关于这个问题的讨论,Why does Java's hashCode() 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, 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是由于它是一个奇质数,若是选择一个偶数会在乘法运算中产生溢出,致使数值信息丢失,由于乘二至关于移位运算。选择质数的优点并非特别的明显,但这是一个传统。同时,数字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 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个,因此在上面几个常数中,常数 31 被 Java 实现所选用也就不足为奇了。
上面的两个答案完美的解释了 Java 源码中选用数字 31 的缘由。接下来,我将针对第二个答案就行验证,请你们继续往下看。
本节,我将使用不一样的数字做为乘子,对超过23万个英文单词进行哈希运算,并计算哈希算法的冲突率。同时,我也将针对不一样乘子算出的哈希值分布状况进行可视化处理,让你们能够直观的看到数据分布状况。本次实验所使用的数据是 Unix/Linux 平台中的英文字典文件,文件路径为 /usr/share/dict/words
。
计算哈希算法冲突率并不难,好比能够一次性将全部单词的 hash code 算出,并放入 Set 中去除重复值。以后拿单词数减去 set.size() 便可得出冲突数,有了冲突数,冲突率就能够算出来了。固然,若是使用 JDK8 提供的流式计算 API,则可更方便算出,代码片断以下:
public static Integer hashCode(String str, Integer multiplier) { int hash = 0; for (int i = 0; i < str.length(); i++) { hash = multiplier * hash + str.charAt(i); } return hash; } /** * 计算 hash code 冲突率,顺便分析一下 hash code 最大值和最小值,并输出 * @param multiplier * @param hashs */ public static void calculateConflictRate(Integer multiplier, List<Integer> hashs) { Comparator<Integer> cp = (x, y) -> x > y ? 1 : (x < y ? -1 : 0); int maxHash = hashs.stream().max(cp).get(); int minHash = hashs.stream().min(cp).get(); // 计算冲突数及冲突率 int uniqueHashNum = (int) hashs.stream().distinct().count(); int conflictNum = hashs.size() - uniqueHashNum; double conflictRate = (conflictNum * 1.0) / hashs.size(); System.out.println(String.format("multiplier=%4d, minHash=%11d, maxHash=%10d, conflictNum=%6d, conflictRate=%.4f%%", multiplier, minHash, maxHash, conflictNum, conflictRate * 100)); }
结果以下:
从上图能够看出,使用较小的质数作为乘子时,冲突率会很高。尤为是质数2,冲突率达到了 55.14%。同时咱们注意观察质数2做为乘子时,哈希值的分布状况。能够看得出来,哈希值分布并非很广,仅仅分布在了整个哈希空间的正半轴部分,即 0 ~ 231-1。而负半轴 -231 ~ -1,则无分布。这也证实了咱们上面断言,即质数2做为乘子时,对于短字符串,生成的哈希值分布性不佳。而后再来看看咱们以前所说的 3一、3七、41 这三个不大不小的质数,表现都不错,冲突数都低于7个。而质数 101 和 199 表现的也很不错,冲突率很低,这也说明哈希值溢出并不必定会致使冲突率上升。可是这两个家伙一言不合就溢出,咱们认为他们不是哈希算法的优选乘子。最后咱们再来看看 32 和 36 这两个偶数的表现,结果并很差,尤为是 32,冲突率超过了了50%。尽管 36 表现的要好一点,不过和 31,37相比,冲突率仍是比较高的。固然并不是全部的偶数做为乘子时,冲突率都会比较高,你们有兴趣能够本身验证。
上一节分析了不一样数字做为乘子时的冲突率状况,这一节来分析一下不一样数字做为乘子时,哈希值的分布状况。在详细分析以前,我先说说哈希值可视化的过程。我本来是打算将全部的哈希值用一维散点图进行可视化,可是后来找了一圈,也没找到合适的画图工具。加以后来想了想,一维散点图可能不合适作哈希值可视化,由于这里有超过23万个哈希值。也就意味着会在图上显示超过23万个散点,若是不出意外的话,这23万个散点会汇集的很密,有可能会变成一个大黑块,就失去了可视化的意义了。因此这里选择了另外一种可视化效果更好的图表,也就是 excel 中的平滑曲线的二维散点图(下面简称散点曲线图)。固然这里一样没有把23万散点都显示在图表上,太多了。因此在实际绘图过程当中,我将哈希空间等分红了64个子区间,并统计每一个区间内的哈希值数量。最后将分区编号作为X轴,哈希值数量为Y轴,就绘制出了我想要的二维散点曲线图了。这里举个例子说明一下吧,以第0分区为例。第0分区数值区间是[-2147483648, -2080374784),咱们统计落在该数值区间内哈希值的数量,获得 <分区编号, 哈希值数量>
数值对,这样就能够绘图了。分区代码以下:
/** * 将整个哈希空间等分红64份,统计每一个空间内的哈希值数量 * @param hashs */ public static Map<Integer, Integer> partition(List<Integer> hashs) { // step = 2^32 / 64 = 2^26 final int step = 67108864; List<Integer> nums = new ArrayList<>(); Map<Integer, Integer> statistics = new LinkedHashMap<>(); int start = 0; for (long i = Integer.MIN_VALUE; i <= Integer.MAX_VALUE; i += step) { final long min = i; final long max = min + step; int num = (int) hashs.parallelStream() .filter(x -> x >= min && x < max).count(); statistics.put(start++, num); nums.add(num); } // 为了防止计算出错,这里验证一下 int hashNum = nums.stream().reduce((x, y) -> x + y).get(); assert hashNum == hashs.size(); return statistics; }
本文中的哈希值是用整形表示的,整形的数值区间是 [-2147483648, 2147483647]
,区间大小为 2^32
。因此这里能够将区间等分红64个子区间,每一个自子区间大小为 2^26
。详细的分区对照表以下:
分区编号 | 分区下限 | 分区上限 | 分区编号 | 分区下限 | 分区上限 |
---|---|---|---|---|---|
0 | -2147483648 | -2080374784 | 32 | 0 | 67108864 |
1 | -2080374784 | -2013265920 | 33 | 67108864 | 134217728 |
2 | -2013265920 | -1946157056 | 34 | 134217728 | 201326592 |
3 | -1946157056 | -1879048192 | 35 | 201326592 | 268435456 |
4 | -1879048192 | -1811939328 | 36 | 268435456 | 335544320 |
5 | -1811939328 | -1744830464 | 37 | 335544320 | 402653184 |
6 | -1744830464 | -1677721600 | 38 | 402653184 | 469762048 |
7 | -1677721600 | -1610612736 | 39 | 469762048 | 536870912 |
8 | -1610612736 | -1543503872 | 40 | 536870912 | 603979776 |
9 | -1543503872 | -1476395008 | 41 | 603979776 | 671088640 |
10 | -1476395008 | -1409286144 | 42 | 671088640 | 738197504 |
11 | -1409286144 | -1342177280 | 43 | 738197504 | 805306368 |
12 | -1342177280 | -1275068416 | 44 | 805306368 | 872415232 |
13 | -1275068416 | -1207959552 | 45 | 872415232 | 939524096 |
14 | -1207959552 | -1140850688 | 46 | 939524096 | 1006632960 |
15 | -1140850688 | -1073741824 | 47 | 1006632960 | 1073741824 |
16 | -1073741824 | -1006632960 | 48 | 1073741824 | 1140850688 |
17 | -1006632960 | -939524096 | 49 | 1140850688 | 1207959552 |
18 | -939524096 | -872415232 | 50 | 1207959552 | 1275068416 |
19 | -872415232 | -805306368 | 51 | 1275068416 | 1342177280 |
20 | -805306368 | -738197504 | 52 | 1342177280 | 1409286144 |
21 | -738197504 | -671088640 | 53 | 1409286144 | 1476395008 |
22 | -671088640 | -603979776 | 54 | 1476395008 | 1543503872 |
23 | -603979776 | -536870912 | 55 | 1543503872 | 1610612736 |
24 | -536870912 | -469762048 | 56 | 1610612736 | 1677721600 |
25 | -469762048 | -402653184 | 57 | 1677721600 | 1744830464 |
26 | -402653184 | -335544320 | 58 | 1744830464 | 1811939328 |
27 | -335544320 | -268435456 | 59 | 1811939328 | 1879048192 |
28 | -268435456 | -201326592 | 60 | 1879048192 | 1946157056 |
29 | -201326592 | -134217728 | 61 | 1946157056 | 2013265920 |
30 | -134217728 | -67108864 | 62 | 2013265920 | 2080374784 |
31 | -67108864 | 0 | 63 | 2080374784 | 2147483648 |
接下来,让咱们对照上面的分区表,对数字二、三、1七、3一、101的散点曲线图进行简单的分析。先从数字2开始,数字2对于的散点曲线图以下:
上面的图仍是很一幕了然的,乘子2算出的哈希值几乎所有落在第32分区,也就是 [0, 67108864)
数值区间内,落在其余区间内的哈希值数量几乎能够忽略不计。这也就不难解释为何数字2做为乘子时,算出哈希值的冲突率如此之高的缘由了。因此这样的哈希算法要它有何用啊,拖出去斩了吧。接下来看看数字3做为乘子时的表现:
3做为乘子时,算出的哈希值分布状况和2很像,只不过稍微好了那么一点点。从图中能够看出绝大部分的哈希值最终都落在了第32分区里,哈希值的分布性不好。这个也没啥用,拖出去枪毙5分钟吧。在看看数字17的状况怎么样:
数字17做为乘子时的表现,明显比上面两个数字好点了。虽然哈希值在第32分区和第34分区有必定的汇集,可是相比较上面2和3,状况明显好好了不少。除此以外,17做为乘子算出的哈希值在其余区也均有分布,且较为均匀,还算是一个不错的乘子吧。
接下来来看看咱们本文的主角31了,31做为乘子算出的哈希值在第33分区有必定的小汇集。不过相比于数字17,主角31的表现又好了一些。首先是哈希值的汇集程度没有17那么严重,其次哈希值在其余区分布的状况也要好于17。总之,选31,准没错啊。
最后再来看看大质数101的表现,不难看出,质数101做为乘子时,算出的哈希值分布状况要好于主角31,有点喧宾夺主的意思。不过不能否认的是,质数101的做为乘子时,哈希值的分布性确实更加均匀。因此若是不在乎质数101容易致使数据信息丢失问题,或许其是一个更好的选择。
通过上面的分析与实践,我想你们应该明白了 String hashCode 方法中选择使用数字31
做为乘子的缘由了。本文本质是一篇简单的科普文而已,并无银弹😁。若是你们读完后以为又涨知识了,那这篇文章的目的就达到了。最后,本篇文章的配图画的仍是很辛苦的,因此若是你们以为文章不错,不妨就给个赞吧,就当是对个人鼓励了。另外,若是文章中有不妥或者错误的地方,也欢迎指出来。若是能不吝赐教,那就更好了。最后祝你们生活愉快,再见。
本文在知识共享许可协议 4.0 下发布,转载请注明出处
做者:coolblog
为了得到更好的分类阅读体验,
请移步至本人的我的博客: http://www.coolblog.xyz
本做品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。