参考文章: 小傅哥,公众号:bugstack 虫洞栈 《java面经手册》

先来了解一下什么是hashCode?

hashCode在Java中所代表的是一种方法,是获得hash值的一种方法。

hash 翻译做“散列”,也可直译为“哈希”,就是把任意长度的值输入,通过hash()函数输出固定长度的消息摘要。 hash函数也有很多种,包括:直接取余法,乘法取整法,平方取中法,暂时先了解有这些方法即可。

hash表是啥? hash表是由hash值组成的。

HashCode的存在主要是为了查找的快捷性,HashCode是用来在散列存储结构中确定对象的存储地址的(用HashCode来代表对象就是在hash表中的位置)。

阅读源码

关于获得haashCode的函数非常之多,我们我截取java.util包中的Arrays类给大家瞅一瞅:

public static int hashCode(long a[]) {

if (a == null)

return 0;

int result = 1;

for (long element : a) {

int elementHash = (int)(element ^ (element >>> 32));

result = 31 * result + elementHash;

}

return result;

}

在获取 hashCode 的源码中可以看到,有一个固定值 31,在 for 循环每次执行时 进行乘积计算,循环后的公式如下; s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

那么这里为什么选择 31 作为乘积值呢?

收集到的资料主要有两个观点:

第一种观点:

31 是一个奇质数,如果选择偶数会导致乘积运算时数据溢出。另外在二进制中,2 个 5 次方是 32,那么也就是 31 * i == (i << 5) - i。这主要是说乘积运算可以使用位移提升性能,同时目前的 JVM 虚拟机 也会自动支持此类的优化。

第二种观点:

经过大量测试,如果用超过 5 千个单词计算 hashCode, 这个 hashCode 的运算使用 31、33、37、39 和 41 作为乘积,得到的碰撞结果,31 被使用就很正常了。

虫洞栈做了一下实验: 统计出不同的乘积数对 10 万个单词的 hash 计算结果。

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 函数对比只是替换了可变参数,用于我们统计不同乘积数的计算结果。

Hash 碰撞概率计算 :

想计算碰撞很简单,也就是计算那些出现相同哈希值的数量,计算出碰撞总量即可。这里的实现方式有很多,可以使用 set、map 也可以使用 java8 的 stream 流 统计distinct。

private static RateInfo hashCollisionRate(Integer multiplier, List 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 值,以及最终返回碰撞数量的统计结果。

单元测试 :

@Before

public void before() {

"abc".hashCode();

// 读取文件,103976 个英语单词库.txt

words = FileUtil.readWordList("E:/itstack/git/github.com/interview/interview01/103976 个英语单词库.txt");

}

@Test

public void test_collisionRate() {

List 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 碰撞结果。

最终得到结果:

以上就是不同的乘数下的 hash 碰撞结果图标展示,从这里可以看出如下信息;乘数是 2 时,hash 的取值范围比较小,基本是堆积到一个范围内了,后 面内容会看到这块的展示。乘数是 3、5、7、17 等,都有较大的碰撞概率乘数是 31 的时候,碰撞的概率已经很小了,基本稳定。顺着往下看,你会发现 199 的碰撞概率更小,这就相当于一排奇数的茅坑量多,自然会减少碰撞。但这个范围值已经远超过 int 的取值范围 了,如果用此数作为乘数,又返回 int 值,就会丢失数据信息。

Hash 值散列分布

除了以上看到哈希值在不同乘数的一个碰撞概率后,关于散列表也就是 hash, 还有一个非常重要的点,那就是要尽可能的让数据散列分布。只有这样才能减少 hash 碰撞次数 。

那么怎么看散列分布呢?如果我们能把 10 万个 hash 值铺到图表上,形成的一张 图,就可以看出整个散列分布。但是这样的图会比较大,当我们缩小看后,就成 一个了大黑点。所以这里我们采取分段统计,把 2 ^ 32 方分 64 个格子进行存 放,每个格子都会有对应的数量的 hash 值,最终把这些数据展示在图表上。

哈希值分段存放

public static Map hashArea(List hashCodeList) {

Map 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-demoany/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 的新特性语法,统计起来还是比较方便的。

单元测试

@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 的散列结果相对来说比较均匀。

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

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

乘数是 199 是不能用的散列结果,但是它的数据是更加分散的,从图上能看到有两个小山包。但因为数据区间问题会有数据丢失问题,所以不能选择。

总结

以上主要介绍了 hashCode 选择 31 作为乘数的主要原因和实验数据验 证,算是一个散列的数据结构的案例讲解,在后续的类似技术中,就可以解释其他的源码设计思路了。看过本文至少应该让你可以从根本上解释了 hashCode 的设计,关于他的 所有问题也就不需要死记硬背了,学习编程内容除了最开始的模仿到深 入以后就需要不断的研究数学逻辑和数据结构。文中参考了优秀的 hashCode 资料和 stackoverflow,并亲自做实验验证结果,大家也可以下载本文中资源内容;英文字典、源码、excel 图表 等内容

2025-05-27 23:27:42