本文将分享为什么Java的String中的hashCode的详细内容,并且还将对使用31作为乘数?进行详尽解释,此外,我们还将为大家带来关于HashCode为什么使用31作为乘数?、hashCode方
本文将分享为什么Java的String中的hashCode的详细内容,并且还将对使用31作为乘数?进行详尽解释,此外,我们还将为大家带来关于HashCode 为什么使用 31 作为乘数?、hashCode方法里为什么选择数字31作为生成hashCode值的乘数、Java 1.7的hashCode()重写不符合我的预期、Java hashCode()和identityHashCode()在后端如何工作?的相关知识,希望对你有所帮助。
本文目录一览:- 为什么Java的String中的hashCode()使用31作为乘数?(java string.hashcode)
- HashCode 为什么使用 31 作为乘数?
- hashCode方法里为什么选择数字31作为生成hashCode值的乘数
- Java 1.7的hashCode()重写不符合我的预期
- Java hashCode()和identityHashCode()在后端如何工作?
为什么Java的String中的hashCode()使用31作为乘数?(java string.hashcode)
每Java文档中,哈希代码的String
对象被计算为:
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
使用
int
算术,其中s[i]
是 我 字符串的个字符,n
是字符串的长度,以及^
表示取幂。
为什么将31用作乘数?
我知道乘数应该是一个相对较大的素数。那么为什么不29或37甚至97?
答案1
小编典典根据约书亚·布洛赫(Joshua
Bloch)的《有效的Java》(这本书不值得推荐,由于对stackoverflow的不断提及,我买了这本书):
选择值31是因为它是奇数质数。如果是偶数且乘法运算溢出,则信息将丢失,因为乘以2等于移位。使用质数的优势尚不清楚,但这是传统的。31的一个不错的特性是乘法可以用移位和减法来代替,以获得更好的性能:
31* i == (i << 5) - i
。现代VM自动执行这种优化。
(摘自第3章第9项:当您覆盖等号时,请始终覆盖哈希码,第48页)
HashCode 为什么使用 31 作为乘数?
持续坚持原创输出,点击蓝字关注我吧
作者:小傅哥
博客:https://bugstack.cn
❝沉淀、分享、成长,让自己和他人都能有所收获!
❞
目录
一、前言
二、面试题
三、资源下载
四、源码讲解
1. 固定乘积 31 在这用到了
2. 来自 stackoverflow 的回答
3. Hash 值碰撞概率统计
4. Hash 值散列分布
五、总结
一、前言
在面经手册的前两篇介绍了「《面试官都问我啥》」和「《认知自己的技术栈盲区》」,这两篇内容主要为了说明面试过程的考查范围,包括个人的自我介绍、技术栈积累、项目经验等,以及在技术栈盲区篇章中介绍了一个整套技术栈在系统架构用的应用,以此全方面的扫描自己有哪些盲区还需要补充。而接下来的章节会以各个系列的技术栈中遇到的面试题作为切入点,讲解技术要点,了解技术原理,包括;数据结构、数据算法、技术栈、框架等进行逐步展开学习。
在进入数据结构章节讲解之前可以先了解下,数据结构都有哪些,基本可以包括;数组(Array)
、栈(Stack)
、队列(Queue)
、链表(LinkList)
、树(Tree)
、散列表(Hash)
、堆(Heap)
、图(Graph)
。
而本文主要讲解的就是与散列表相关的 HashCode
,本来想先讲 HashMap
,但随着整理资料发现与 HashMap
的实现中,HashCode
的散列占了很重要的一设计思路,所以最好把这部分知识补全,再往下讲解。
二、面试题
说到 HashCode 的面试题,可能这是一个非常核心的了。其他考点;怎么实现散列、计算逻辑等,都可以通过这道题的学习了解相关知识。
「Why does Java''s hashCode() in String use 31 as a multiplier?」

「这个问题其实☞指的就是,hashCode 的计算逻辑中,为什么是 31 作为乘数。」

三、资源下载
本文讲解的过程中涉及部分源码等资源,可以通过关注公众号:bugstack虫洞栈
,回复下载进行获取 {回复下载后打开获得的链接,找到编号 ID:19},包括;
-
HashCode 源码测试验证工程, interview-03
-
「103976 个英语单词库.txt」,验证 HashCode 值 -
「HashCode 散列分布.xlsx」,散列和碰撞图表
四、源码讲解
1. 固定乘积 31 在这用到了
// 获取hashCode "abc".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;
}
在获取 hashCode
的源码中可以看到,有一个固定值 31
,在 for 循环每次执行时进行乘积计算,循环后的公式如下;s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
「那么这里为什么选择 31 作为乘积值呢?」
2. 来自 stackoverflow 的回答
在 stackoverflow
关于为什么选择 31 作为固定乘积值,有一篇讨论文章,Why does Java''s hashCode () in String use 31 as a multiplier? 这是一个时间比较久的问题了,摘取两个回答点赞最多的;
「413 个赞的回答」
最多的这个回答是来自《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 是一个奇质数,如果选择偶数会导致乘积运算时数据溢出。 -
另外在二进制中,2 个 5 次方是 32,那么也就是 31 * i == (i << 5) - i
。这主要是说乘积运算可以使用位移提升性能,同时目前的 JVM 虚拟机也会自动支持此类的优化。
「80 个赞的回答」
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.
-
这个回答就很有实战意义了,告诉你用超过 5 千个单词计算 hashCode,这个 hashCode 的运算使用 31、33、37、39 和 41 作为乘积,得到的碰撞结果,31 被使用就很正常了。 -
「他这句话就就可以作为我们实践的指向了。」
3. Hash 值碰撞概率统计
接下来要做的事情并不难,只是根据 stackoverflow
的回答,统计出不同的乘积数对 10 万个单词的 hash 计算结果。10 个单词表已提供,可以通过关注公众号:bugstack 虫洞栈进行下载
3.1 读取单词字典表
1 a "n.(A)As 或 A''s 安(ampere(a) art.一;n.字母A /[军] Analog.Digital,模拟/数字 /(=account of) 帐上"
2 aaal American Academy of Arts and Letters 美国艺术和文学学会
3 aachen 亚琛[德意志联邦共和国西部城市]
4 aacs Airways and Air Communications Service (美国)航路与航空通讯联络处
5 aah " [军]Armored Artillery Howitzer,装甲榴弹炮;[军]Advanced Attack Helicopter,先进攻击直升机"
6 aal "ATM Adaptation Layer,ATM适应层"
7 aapamoor "n.[生]丘泽,高低位镶嵌沼泽"
-
单词表的文件格式如上,可以自行解析 -
读取文件的代码比较简单,这里不展示了,可以通过 资源下载
进行获取
3.2 Hash 计算函数
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 函数对比只是替换了可变参数,用于我们统计不同乘积数的计算结果。
3.3 Hash 碰撞概率计算
想计算碰撞很简单,也就是计算那些出现相同哈希值的数量,计算出碰撞总量即可。这里的实现方式有很多,可以使用 set
、map
也可以使用 java8
的 stream
流统计 distinct
。
private static RateInfo hashCollisionRate(Integer multiplier, List<Integer> hashCodeList) {
int maxHash = hashCodeList.stream().max(Integer::compareTo).get();
int minHash = hashCodeList.stream().min(Integer::compareTo).get();
int collisionCount = (int) (hashCodeList.size() - hashCodeList.stream().distinct().count());
double collisionRate = (collisionCount * 1.0) / hashCodeList.size();
return new RateInfo(maxHash, minHash, multiplier, collisionCount, collisionRate);
}
-
这里记录了最大 hash 和最小 hash 值,以及最终返回碰撞数量的统计结果。
3.4 单元测试
@Before
public void before() {
"abc".hashCode();
// 读取文件,103976个英语单词库.txt
words = FileUtil.readWordList("E:/itstack/git/github.com/interview/interview-01/103976个英语单词库.txt");
}
@Test
public void test_collisionRate() {
List<RateInfo> rateInfoList = HashCode.collisionRateList(words, 2, 3, 5, 7, 17, 31, 32, 33, 39, 41, 199);
for (RateInfo rate : rateInfoList) {
System.out.println(String.format("乘数 = %4d, 最小Hash = %11d, 最大Hash = %10d, 碰撞数量 =%6d, 碰撞概率 = %.4f%%", rate.getMultiplier(), rate.getMinHash(), rate.getMaxHash(), rate.getCollisionCount(), rate.getCollisionRate() * 100));
}
}
-
以上先设定读取英文单词表中的 10 个单词,之后做 hash 计算。 -
在 hash 计算中把单词表传递进去,同时还有乘积数; 2, 3, 5, 7, 17, 31, 32, 33, 39, 41, 199
,最终返回一个 list 结果并输出。 -
这里主要验证同一批单词,对于不同乘积数会有怎么样的 hash 碰撞结果。
「测试结果」
单词数量:103976
乘数 = 2, 最小Hash = 97, 最大Hash = 1842581979, 碰撞数量 = 60382, 碰撞概率 = 58.0730%
乘数 = 3, 最小Hash = -2147308825, 最大Hash = 2146995420, 碰撞数量 = 24300, 碰撞概率 = 23.3708%
乘数 = 5, 最小Hash = -2147091606, 最大Hash = 2147227581, 碰撞数量 = 7994, 碰撞概率 = 7.6883%
乘数 = 7, 最小Hash = -2147431389, 最大Hash = 2147226363, 碰撞数量 = 3826, 碰撞概率 = 3.6797%
乘数 = 17, 最小Hash = -2147238638, 最大Hash = 2147101452, 碰撞数量 = 576, 碰撞概率 = 0.5540%
乘数 = 31, 最小Hash = -2147461248, 最大Hash = 2147444544, 碰撞数量 = 2, 碰撞概率 = 0.0019%
乘数 = 32, 最小Hash = -2007883634, 最大Hash = 2074238226, 碰撞数量 = 34947, 碰撞概率 = 33.6106%
乘数 = 33, 最小Hash = -2147469046, 最大Hash = 2147378587, 碰撞数量 = 1, 碰撞概率 = 0.0010%
乘数 = 39, 最小Hash = -2147463635, 最大Hash = 2147443239, 碰撞数量 = 0, 碰撞概率 = 0.0000%
乘数 = 41, 最小Hash = -2147423916, 最大Hash = 2147441721, 碰撞数量 = 1, 碰撞概率 = 0.0010%
乘数 = 199, 最小Hash = -2147459902, 最大Hash = 2147480320, 碰撞数量 = 0, 碰撞概率 = 0.0000%
Process finished with exit code 0

以上就是不同的乘数下的 hash 碰撞结果图标展示,从这里可以看出如下信息;
-
乘数是 2 时,hash 的取值范围比较小,基本是堆积到一个范围内了,后面内容会看到这块的展示。 -
乘数是 3、5、7、17 等,都有较大的碰撞概率 -
「乘数是 31 的时候,碰撞的概率已经很小了,基本稳定。」 -
顺着往下看,你会发现 199 的碰撞概率更小,这就相当于一排奇数的茅坑量多,自然会减少碰撞。 「但这个范围值已经远超过 int 的取值范围了,如果用此数作为乘数,又返回 int 值,就会丢失数据信息」。
4. Hash 值散列分布
除了以上看到哈希值在不同乘数的一个碰撞概率后,关于散列表也就是 hash,还有一个非常重要的点,那就是要尽可能的让数据散列分布。只有这样才能减少 hash 碰撞次数,也就是后面章节要讲到的 hashMap 源码。
那么怎么看散列分布呢?如果我们能把 10 万个 hash 值铺到图表上,形成的一张图,就可以看出整个散列分布。但是这样的图会比较大,当我们缩小看后,就成一个了大黑点。所以这里我们采取分段统计,把 2 ^ 32 方分 64 个格子进行存放,每个格子都会有对应的数量的 hash 值,最终把这些数据展示在图表上。
4.1 哈希值分段存放
public static Map<Integer, Integer> hashArea(List<Integer> hashCodeList) {
Map<Integer, Integer> statistics = new LinkedHashMap<>();
int start = 0;
for (long i = 0x80000000; i <= 0x7fffffff; i += 67108864) {
long min = i;
long max = min + 67108864;
// 筛选出每个格子里的哈希值数量,java8流统计;https://bugstack.cn/itstack-demo-any/2019/12/10/%E6%9C%89%E7%82%B9%E5%B9%B2%E8%B4%A7-Jdk1.8%E6%96%B0%E7%89%B9%E6%80%A7%E5%AE%9E%E6%88%98%E7%AF%87(41%E4%B8%AA%E6%A1%88%E4%BE%8B).html
int num = (int) hashCodeList.parallelStream().filter(x -> x >= min && x < max).count();
statistics.put(start++, num);
}
return statistics;
-
这个过程主要统计 int
取值范围内,每个哈希值存放到不同格子里的数量。 -
这里也是使用了 java8 的新特性语法,统计起来还是比较方便的。
4.2 单元测试
@Test
public void test_hashArea() {
System.out.println(HashCode.hashArea(words, 2).values());
System.out.println(HashCode.hashArea(words, 7).values());
System.out.println(HashCode.hashArea(words, 31).values());
System.out.println(HashCode.hashArea(words, 32).values());
System.out.println(HashCode.hashArea(words, 199).values());
}
-
这里列出我们要统计的乘数值,每一个乘数下都会有对应的哈希值数量汇总,也就是 64 个格子里的数量。 -
最终把这些统计值放入到 excel 中进行图表化展示。
「统计图表」

-
以上是一个堆积百分比统计图,可以看到下方是不同乘数下的,每个格子里的数据统计。 -
除了 199 不能用以外,31 的散列结果相对来说比较均匀。
4.2.1 乘数 2 散列

-
乘数是 2 的时候,散列的结果基本都堆积在中间,没有很好的散列。
4.2.2 乘数 31 散列

-
乘数是 31 的时候,散列的效果就非常明显了,基本在每个范围都有数据存放。
4.2.3 乘数 199 散列

-
乘数是 199 是不能用的散列结果,但是它的数据是更加分散的,从图上能看到有两个小山包。但因为数据区间问题会有数据丢失问题,所以不能选择。
「文中引用」
-
http://www.tianxiaobo.com/2018/01/18/String-hashCode-%E6%96%B9%E6%B3%95%E4%B8%BA%E4%BB%80%E4%B9%88%E9%80%89%E6%8B%A9%E6%95%B0%E5%AD%9731%E4%BD%9C%E4%B8%BA%E4%B9%98%E5%AD%90/ -
https://stackoverflow.com/questions/299304/why-does-javas-hashcode-in-string-use-31-as-a-multiplier
五、总结
-
以上主要介绍了 hashCode 选择 31 作为乘数的主要原因和实验数据验证,算是一个散列的数据结构的案例讲解,在后续的类似技术中,就可以解释其他的源码设计思路了。 -
看过本文至少应该让你可以从根本上解释了 hashCode 的设计,关于他的所有问题也就不需要死记硬背了,学习编程内容除了最开始的模仿到深入以后就需要不断的研究数学逻辑和数据结构。 -
文中参考了优秀的 hashCode 资料和 stackoverflow,并亲自做实验验证结果,大家也可以下载本文中资源内容;英文字典、源码、excel 图表等内容。
bugstack 虫洞栈
沉淀、分享、成长,让自己和他人都能有所收获!
作者小傅哥多年从事一线互联网Java
开发,从 19 年开始编写工作和学习历程的技术汇总,旨在为大家提供一个较清晰详细的核心技能学习文档。如果本文能为您提供帮助,请给予支持 (关注、点赞、分享)!
感谢支持小傅哥原创,欢迎点击在看和转发
本文分享自微信公众号 - bugstack 虫洞栈(bugstack)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与 “OSC 源创计划”,欢迎正在阅读的你也加入,一起分享。
hashCode方法里为什么选择数字31作为生成hashCode值的乘数
前提:
偶然的机会看到了大神的一篇博客,介绍的是hashCode()方法里为什么要用31这个数字作为生成hashCode的乘数。hashCode我在比较自定义类时曾经用到过 - 由于java默认比较的是类的地址值,每个对象一定是不同的,所以重写了hashCode()和equals()方法
,这样就会先根据类里的属性生成hashCode,如果生成的hashCode值相同,则在使用equals()比较属性的值。两者都相同则认为这两个对象相等。当是就对hashCode的生成很好奇,自己看了下源码也是懵懵懂懂。这下终于可以搞清楚了,开森,感谢大佬!
hashCode()方法的源码:
原因一:更少的乘积结果冲突
31是质子数中一个“不大不小”的存在,如果你使用的是一个如2的较小质数,那么得出的乘积会在一个很小的范围,很容易造成哈希值的冲突。而如果选择一个100以上的质数,得出的哈希值会超出int的最大范围,这两种都不合适。而如果对超过 50,000 个英文单词(由两个不同版本的 Unix 字典合并而成)进行 hash code 运算,并使用常数 31, 33, 37, 39 和 41 作为乘子,每个常数算出的哈希值冲突数都小于7个(国外大神做的测试),那么这几个数就被作为生成hashCode值得备选乘数了。
原因二:31可以被JVM优化
JVM里最有效的计算方式就是进行位运算了:
* 左移 << : 左边的最高位丢弃,右边补全0(把 << 左边的数据*2的移动次幂)。
* 右移 >> : 把>>左边的数据/2的移动次幂。
* 无符号右移 >>> : 无论最高位是0还是1,左边补齐0。
所以 : 31 * i = (i << 5) - i(左边 31*2=62,右边 2*2^5-2=62) - 两边相等,JVM就可以高效的进行计算啦。。。
ps:以前家里人老说学号数理化,走遍全天下。貌似很有道理^_^
Java 1.7的hashCode()重写不符合我的预期
我有一个我重写了hashCode方法和equals方法的类。equals方法的行为符合我的预期,但是hashCode方法的行为似乎不符合我的预期。因此,我假设我的期望是不正确的,但不确定原因。下面是重写的方法:
public class Car extends SomeBaseClass implements Cloneable, Serializable {private static final long serialVersionUID = 1L;private String id;private String name;private String carName;private String carModel;private String displayTextCar;public boolean equals(Car car){ return (getCarName().equals(car.getCarName()) && getCarModel().equals(car.getCarModel()));}public int hashCode(){ return (this.getCarName() + this.getCarModel()).hashCode();}
现在,我有一个测试类,其中创建两个car对象,并调用equals方法,然后将car的每个实例放入HashMap中。我将每个实例设置为具有相同的汽车名称和模型,并调用equals方法实际上返回true。但是,即使每个实例返回相同的hashCode,当我将它们添加到HashMap时,它仍将两个对象保留在Map中,而我希望第二个放置项可以替换地图中的第一个对象。以下是测试课程的内容:
HashMap<Car,String> testMap;Car testCar1 = new Car();testCar1.setCarName("DaveCar");testCar1.setCarModel("DaveModelTest");System.out.println("Car Hash 1: " + testCar1.hashCode());Car testCar2 = new Car();testCar2.setCarName("DaveCar");testCar2.setCarModel("DaveModelTest");System.out.println("Car Hash 2: " + testCar2.hashCode());//hashCodes prints identical numbersSystem.out.println("Car 1 equal Car 2 ?? " + testCar1.equals(testCar2));//returns truetestMap.put(testCar1, "3"); testMap.put(testCar2, "16");System.out.println("Map size is " + testMap.size());//I would expect the size to be 1 here, but it''s in fact 2.
所以这对我来说似乎不正确,我自然在这里省略了一些代码,但这是基本原理。希望有人可以指出我在这里出了问题。请注意,我确实使用Eclipse生成了hashCode和equals方法,并且可以正常工作,但是令我感到困惑的是,即使两个对象似乎为hashCode返回了相同的值,我的hashCode的实现也没有按我的预期工作。感谢任何人的输入。
答案1
小编典典问题是您提供了一个错误equals
:应该equals(Object)
,而不是equals(Car)
。
本质上,您提供了一个 重载 而不是 override ,因此HashMap
始终equals
从基类中调用。
解决此问题的方法很简单:添加一个执行强制转换的覆盖,并调用equals
您编写的方法,如下所示:
@Overridepublic boolean equals(Object other) { return (other instanceof Car) && equals((Car)other);}
注意注释的使用@Override
。它可以帮助Java帮助您自动发现此类问题。
注意:对于这个问题,请考虑hashCode
以更“节省”的方式实现您的方法。与其(this.getCarName() +this.getCarModel())
仅出于获取其哈希码的目的而创建丢弃字符串,还不如考虑如下重写该方法:
public int hashCode() { return 31*getCarName().hashCode() + getCarModel().hashCode();}
或在Java 1.7+中,您可以编写
public int hashCode() { // Thanks, fge, for a nice improvement! return Objects.hash(carName, carModel);}
Java hashCode()和identityHashCode()在后端如何工作?
如何做Object.hashCode()
和System.identityHashCode()
工作在后端?是否identityHashCode()
返回对象的引用?是否hashCode()
取决于?对象的?==操作员如何在后端工作。
hashCode()
和之间有什么区别identityHashCode()
?
答案1
小编典典后端的Object.hashCode()
和System.identityHashCode()
如何工作?
假设尚未覆盖,则该Object.hashCode()
方法只需调用即可System.identityHashCode(this)
。
的确切行为System.identityHashCode(Object)
取决于JVM实现。(在最近的Hotspot JVM上的实际实现是相当聪明的,但是我离题了。)
是否identityHashCode()
返回对象的引用?
否。它返回int,而an int不能保存引用。
返回的整数identityHashCode
可能与对象的(a)机器地址有关,或者可能不是1。identityHashCode()
保证返回的值在对象的生存期内不会改变。这意味着,如果GC(在identityHashCode()
调用之后)重定位对象,则它不能使用新的对象地址作为身份哈希码。
hashCode()是否取决于?对象? ==运算符的后端工作方式。
这没有道理。Java中没有? ==
或?==
运算符。
hashCode()和identityHashCode()有什么区别?
以上已部分解释。其他差异包括:
该
hashcode()
方法是非最终实例方法,并且在覆盖的任何类中都应覆盖此equals(Object)
方法。相反,identityHashCode(Object)
是一种static
方法,因此不能被覆盖。该
identityHashCode(Object)
方法为你提供了一个对象的标识符,该标识符在理论上可以用于哈希和哈希表以外的其他用途。(不幸的是,它不是一个独特的标识符,但被保证为对象的生命周期永远不会改变。)
我们今天的关于为什么Java的String中的hashCode和使用31作为乘数?的分享已经告一段落,感谢您的关注,如果您想了解更多关于HashCode 为什么使用 31 作为乘数?、hashCode方法里为什么选择数字31作为生成hashCode值的乘数、Java 1.7的hashCode()重写不符合我的预期、Java hashCode()和identityHashCode()在后端如何工作?的相关信息,请在本站查询。
本文标签: