HashCode 为什么使用 31 作为乘数?

参考文章: 小傅哥,公众号: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 作为乘积值呢?

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

第一种观点:

  1. 31 是一个奇质数,如果选择偶数会导致乘积运算时数据溢出。
  2. 另外在二进制中,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<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 值,以及最终返回碰撞数量的统计结果。

单元测试 :

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

最终得到结果:

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

Hash 值散列分布

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

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

哈希值分段存放

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-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 图表 等内容
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: HashCode是一个Java中的方法,用于返回对象的哈希码值。它通常用于确定一个对象的唯一标识符,以便在需要比较对象时快速查找和识别该对象。在使用集合框架时,哈希码值在查找元素时非常有用,可以大大提高查找效率。 ### 回答2: hashcodeJava中Object类中的一个方法,用于计算对象的哈希码值。 哈希码是一个用于标识对象的整数值。它是根据对象的内部状态(即对象的属性值)计算得出的。同样的输入会得到相同的哈希码,不同的输入会得到不同的哈希码。 hashcode方法的作用主要有两点: 1. 在集合框架中,如HashMap、HashSet等类,使用哈希码来确定对象的存储位置。哈希码作为一个索引,能够快速地定位对象所在的存储位置,提高了集合类的存取效率。 2. 在对象比较时,equals方法通常与hashcode方法一起使用。equals被用来判断两个对象是否相等,而hashcode则被用来判断两个对象是否“可能相等”。在重写equals方法时,一般也需要同时重写hashcode方法,以保证对象相等时它们的哈希码也相等。这样可以确保对象作为Map的键值或者放入HashSet等集合类中时能够正确地进行查找和去重等操作。 需要注意的是,哈希码并不是唯一的,即不同的对象可能会生成相同的哈希码。因此,在编写代码时不能依赖于哈希码的唯一性,而应当保证equals方法的正确性,以确保判断对象是否相等时的准确性。 ### 回答3: hashcode是对象在内存中的唯一标识符,它是通过哈希函数计算出来的一个整数值。在Java中,hashcode是由Object类的hashCode()方法生成的。 hashcode的作用有两个方面: 1. 在哈希表中用作对象的存储索引。哈希表是一种常用的数据结构,它可以快速存储和查询数据。哈希函数将对象映射到一个特定的索引位置,不同的对象会得到不同的hashcode,从而实现快速的存储和查找。 2. 在集合类中用来快速定位元素。集合类如HashMap、HashSet等都是基于哈希表实现的,它们通过对象的hashcode来确定元素的存储位置。当需要查找或删除元素时,只需要计算待查找对象的hashcode,就可以快速定位到对应的存储位置,从而提高了操作的效率。 需要注意的是,hashcode并不是唯一的,不同的对象可能会得到相同的hashcode值,这就是所谓的哈希冲突。为了解决冲突,Java中提供了equals()方法来判断两个对象是否相等。当两个对象的hashcode相等且equals方法返回true时,这两个对象被认为是相等的。因此,对于自定义的类,我们需要重写hashCode()和equals()方法,保证其正确性和一致性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mr.huang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值