HashCode为什么使用31作为乘数
1. 前言
HashMap是用来存储键值对的数据结构,是用哈希表来实现的,可以实现O(1)时间复杂度的插入和查找。哈希表的基本原理即通过计算key的哈希值将元素插入到指定位置,因此在分析HashMap源码之前需要先分析一下哈希值的计算过程。
查看String类的源码可以发现,它重写了hashcode方法:
1 | |
可以看到String类的hashcode方法遍历字符串的每个字符做与31做乘积运算,公式如下:$s[0]^{n-1}+s[1]^{n-2}+…s[n-1]$,那这里为什么要使用31作为乘数呢?
stackoverflow中有一篇讨论文章Why does Java’s hashCode() in String use 31 as a multiplier? - Stack Overflow,其中有一个高赞回答:
Goodrich and Tamassia computed from over 50,000 English words (formed as the union of the word lists provided in two variants of Unix) that using the constants 31, 33, 37, 39, and 41 will produce fewer than 7 collisions in each case. This may be the reason that so many Java implementations choose such constants.
即计算超过50000个单词的hashcode,分别使用31,33,37,39作为乘数,计算碰撞结果。下面我们就来做这个实验,观察不同乘数的碰撞结果。
2. hashcode碰撞概率统计
2.1 计算hashcode碰撞概率
首先准备10万个单词:
1 | |
读取单词:
1 | |
hashcode函数:
1 | |
计算hash碰撞概率:
1 | |
这里RateInfo为自定义类:
1 | |
2.2 单元测试
编写单元测试:
1 | |
测试结果:
1 | |
根据不同乘数的碰撞概率绘制图像如下:
根据结果可以看出,乘数为2、3、5、7、17、32时,碰撞概率都较大,乘数为31、33、39、41、199时,碰撞概率较小,但是乘数较大时,hashcode也会溢出,导致丢失信息。
3. hashcode散列分布
除了计算不同乘数下的hashcode碰撞概率,还需要计算hashcode的分布,只有当hashcode分布均匀时,才能减少碰撞。
为了计算hashcode的散列分布,可以将int的取值范围2^64分成64份,统计每个范围的hashcode数量即可。
3.1 哈希值分段存放
计算每个区间的hashcode数量:
1 | |
3.2 单元测试
1 | |
上面计算了不同乘数在每个区间的hashcode数量,绘制图像如下:
可以看到乘数为2、7、32时,hashcode分布都不是很均匀,乘数为31、199时分布较均匀,只有部分区间包含较多元素,但是使用199作为乘数会导致数据溢出,丢失信息,不能选择,因此选择31作为乘数是比较适合的。
4. 总结
本文主要分析了String类中的hashcode为什么使用31作为乘数,通过使用不同的乘数计算hashcode,统计hashcode的碰撞概率和散列分布情况,得出结论:使用31作为乘数时,hashcode的碰撞概率较小,分布区间较均匀,并且hashcode的数据范围较小,不易产生溢出和数据丢失问题。