String hashCode 方法为什么选择数字31作为乘子Word格式.docx
《String hashCode 方法为什么选择数字31作为乘子Word格式.docx》由会员分享,可在线阅读,更多相关《String hashCode 方法为什么选择数字31作为乘子Word格式.docx(10页珍藏版)》请在冰豆网上搜索。
![String hashCode 方法为什么选择数字31作为乘子Word格式.docx](https://file1.bdocx.com/fileroot1/2022-12/29/a7e7e825-b4da-4610-ba76-7fa2836d4225/a7e7e825-b4da-4610-ba76-7fa2836d42251.gif)
这里我来简单推导一下这个公式:
假设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的理由。
从网上的资料来看,一般有如下两个原因:
第一,31是一个不大不小的质数,是作为hashCode乘子的优选质数之一。
另外一些相近的质数,比如37、41、43等等,也都是不错的选择。
那么为啥偏偏选中了31呢?
请看第二个原因。
第二、31可以被JVM优化,31*i=(i<
<
5)-i。
上面两个原因中,第一个需要解释一下,第二个比较简单,就不说了。
下面我来解释第一个理由。
一般在设计哈希算法时,会选择一个特殊的质数。
至于为啥选择质数,我想应该是可以降低哈希算法的冲突率。
至于原因,这个就要问数学家了,我几乎可以忽略的数学水平解释不了这个原因。
上面说到,31是一个不大不小的质数,是优选乘子。
那为啥同是质数的2和101(或者更大的质数)就不是优选乘子呢,分析如下。
这里先分析质数2。
首先,假设
n=6,然后把质数2和n带入上面的计算公式。
并仅计算公式中次数最高的那一项,结果是2^5=32,是不是很小。
所以这里可以断定,当字符串长度不是很长时,用质数2做为乘子算出的哈希值,数值不会很大。
也就是说,哈希值会分布在一个较小的数值区间内,分布性不佳,最终可能会导致冲突率上升。
上面说了,质数2做为乘子会导致哈希值分布在一个较小区间内,那么如果用一个较大的大质数101会产生什么样的结果呢?
根据上面的分析,我想大家应该可以猜出结果了。
就是不用再担心哈希值会分布在一个小的区间内了,因为101^5=10,510,100,501。
但是要注意的是,这个计算结果太大了。
如果用int类型表示哈希值,结果会溢出,最终导致数值信息丢失。
尽管数值信息丢失并不一定会导致冲突率上升,但是我们暂且先认为质数101(或者更大的质数)也不是很好的选择。
最后,我们再来看看质数31的计算结果:
31^5=28629151,结果值相对于32和10,510,100,501来说。
是不是很nice,不大不小。
上面用了比较简陋的数学手段证明了数字31是一个不大不小的质数,是作为hashCode乘子的优选质数之一。
接下来我会用详细的实验来验证上面的结论,不过在验证前,我们先看看StackOverflow上关于这个问题的讨论,WhydoesJava'
shashCode()inStringuse31asamultiplier?
。
其中排名第一的答案引用了《EffectiveJava》中的一段话,这里也引用一下:
Thevalue31waschosenbecauseitisanoddprime.Ifitwereevenandthemultiplicationoverflowed,informationwouldbelost,asmultiplicationby2isequivalenttoshifting.Theadvantageofusingaprimeislessclear,butitistraditional.Anicepropertyof31isthatthemultiplicationcanbereplacedbyashiftandasubtractionforbetterperformance:
`31*i==(i<
5)-i``.ModernVMsdothissortofoptimizationautomatically.
简单翻译一下:
选择数字31是因为它是一个奇质数,如果选择一个偶数会在乘法运算中产生溢出,导致数值信息丢失,因为乘二相当于移位运算。
选择质数的优势并不是特别的明显,但这是一个传统。
同时,数字31有一个很好的特性,即乘法运算可以被移位和减法运算取代,来获取更好的性能:
31*i==(i<
5)-i,现代的Java虚拟机可以自动的完成这个优化。
排名第二的答案设这样说的:
AsGoodrichandTamassiapointout,Ifyoutakeover50,000Englishwords(formedastheunionofthewordlistsprovidedintwovariantsofUnix),usingtheconstants31,33,37,39,and41willproducelessthan7collisionsineachcase.Knowingthis,itshouldcomeasnosurprisethatmanyJavaimplementationschooseoneoftheseconstants.
这段话也翻译一下:
正如Goodrich和Tamassia指出的那样,如果你对超过50,000个英文单词(由两个不同版本的Unix字典合并而成)进行hashcode运算,并使用常数31,33,37,39和41作为乘子,每个常数算出的哈希值冲突数都小于7个,所以在上面几个常数中,常数31被Java实现所选用也就不足为奇了。
上面的两个答案完美的解释了Java源码中选用数字31的原因。
接下来,我将针对第二个答案就行验证,请大家继续往下看。
3.实验及数据可视化
本节,我将使用不同的数字作为乘子,对超过23万个英文单词进行哈希运算,并计算哈希算法的冲突率。
同时,我也将针对不同乘子算出的哈希值分布情况进行可视化处理,让大家可以直观的看到数据分布情况。
本次实验所使用的数据是Unix/Linux平台中的英文字典文件,文件路径为
/usr/share/dict/words。
3.1哈希值冲突率计算
计算哈希算法冲突率并不难,比如可以一次性将所有单词的hashcode算出,并放入Set中去除重复值。
之后拿单词数减去set.size()即可得出冲突数,有了冲突数,冲突率就可以算出来了。
当然,如果使用JDK8提供的流式计算API,则可更方便算出,代码片段如下:
publicstaticIntegerhashCode(Stringstr,Integermultiplier){
inthash=0;
str.length();
hash=multiplier*hash+str.charAt(i);
returnhash;
/**
*计算hashcode冲突率,顺便分析一下hashcode最大值和最小值,并输出
*@parammultiplier
*@paramhashs
*/
publicstaticvoidcalculateConflictRate(Integermultiplier,List<
Integer>
hashs){
Comparator<
cp=(x,y)->
x>
y?
1:
(x<
-1:
0);
intmaxHash=hashs.stream().max(cp).get();
intminHash=hashs.stream().min(cp).get();
//计算冲突数及冲突率
intuniqueHashNum=(int)hashs.stream().distinct().count();
intconflictNum=hashs.size()-uniqueHashNum;
doubleconflictRate=(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作为乘子时,对于短字符串,生成的哈希值分布性不佳。
然后再来看看我们之前所说的31、37、41这三个不大不小的质数,表现都不错,冲突数都低于7个。
而质数101和199表现的也很不错,冲突率很低,这也说明哈希值溢出并不一定会导致冲突率上升。
但是这两个家伙一言不合就溢出,我们认为他们不是哈希算法的优选乘子。
最后我们再来看看32和36这两个偶数的表现,结果并不好,尤其是32,冲突率超过了了50%。
尽管36表现的要好一点,不过和31,37相比,冲突率还是比较高的。
当然并非所有的偶数作为乘子时,冲突率都会比较高,大家有兴趣可以自己验证。
3.2哈希值分布可视化
上一节分析了不同数字作为乘子时的冲突率情况,这一节来分析一下不同数字作为乘子时,哈希值的分布情况。
在详细分析之前,我先说说哈希值可视化的过程。
我原本是打算将所有的哈希值用一维散点图进行可视化,但是后来找了一圈,也没找到合适的画图工具。
加之后来想了想,一维散点图可能不合适做哈希值可视化,因为这里有超过23万个哈希值。
也就意味着会在图上显示超过23万个散点,如果不出意外的话,这23万个散点会聚集的很密,有可能会变成一个大黑块,就失去了可视化的意义了。
所以这里选择了另一种可视化效果更好的图表,也就是excel中的平滑曲线的二维散点图(下面简称散点曲线图)。
当然这里同样没有把23万散点都显示在图表上,太多了。
所以在实际绘图过程中,我将哈希空间等分成了64个子区间,并统计每个区间内的哈希值数量。
最后将分区编号做为X轴,哈希值数量为Y轴,就绘制出了我想要的二维散点曲线图了。
这里举个例子说明一下吧,以第0分区为例。
第0分区数值区间是[-2147483648,-2080374784),我们统计落在该数值区间内哈希值的数量,得到
分区编号,哈希值数量>
数值对,这样就可以绘图了。
分区代码如下:
/**
*将整个哈希空间等分成64份,统计每个空间内的哈希值数量
publicstaticMap<
Integer,Integer>
partition(List<
//step=2^32/64=2^26
finalintstep=67108864;
List<
nums=newArrayList<
>
();
Map<
statistics=newLinkedHashMap<
intstart=0;
for(longi=Integer.MIN_VALUE;
=Integer.MAX_VALUE;
i+=step){
finallongmin=i;
finallongmax=min+step;
intnum=(int)hashs.parallelStream()
.filter(x->
=min&
x<
max).count();
statistics.put(start++,num);
nums.add(num);
//为了防止计算出错,这里验证一下
inthashNum=nums.stream().reduce((x,y)->
x+y).get();
asserthashNum==hashs.size();
returnstatistics;
本文中的哈希值是用整形表示的,整形的数值区间是
[-2147483648,2147483647],区间大小为
2^32。
所以这里可以将区间等分成64个子区间,每个自子区间大小为
2^26。
详细的分区对照表如下:
分区编号
分区下限
分区上限
-2147483648
-2080374784
32
67108864
1
-2013265920
33
134217728
2
-1946157056
34
201326592
3
-1879048192
35
268435456
4
-1811939328
36
335544320
5
-1744830464
37
402653184
6
-1677721600
38
469762048
7
-1610612736
39
536870912
8
-1543503872
40
603979776
9
-1476395008
41
671088640
10
-1409286144
42
738197504
11
-1342177280
43
805306368
12
-1275068416
44
872415232
13
-1207959552
45
939524096
14
-1140850688
46
1006632960
15
-1073741824
47
1073741824
16
-1006632960
48
1140850688
17
-939524096
49
1207959552
18
-872415232
50
1275068416
19
-805306368
51
1342177280
20
-738197504
52
1409286144
21
-671088640
53
1476395008
22
-603979776
54
1543503872
23
-536870912
55
1610612736
24
-469762048
56
1677721600
25
-402653184
57
1744830464
26
-335544320
58
1811939328
27
-268435456
59
1879048192
28
-201326592
60
1946157056
29
-134217728
61
2013265920
30
-67108864
62
2080374784
31
63
2147483648
接下来,让我们对照上面的分区表,对数字2、3、17、31、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容易导致数据信息丢失问题,或许其是一个更好的选择。
4.写在最后
经过上面的分析与实践,我想大家应该明白了StringhashCode方法中选择使用数字31作为乘子的原因了。
本文本质是一篇简单的科普文而已,并没有银弹。
如果大家读完后觉得又涨知识了,那这篇文章的目的就达到了。
最后,本篇文章的配图画的还是很辛苦的,所以如果大家觉得文章不错,不妨就给个赞吧,就当是对我的鼓励了。
另外,如果文章中有不妥或者错误的地方,也欢迎指出来。
如果能不吝赐教,那就更好了。
最后祝大家生活愉快,再见。