作者:小傅哥

博客:https://bugstack.cn

沉淀、分享、成长,让自己和他人都能有所收获!

一、前言

在面经手册的前两篇介绍了《面试官都问我啥》《认知自己的技术栈盲区》,这两篇内容主要为了说明面试过程的考查范围,包括个人的自我介绍、技术栈积累、项目经验等,以及在技术栈盲区篇章中介绍了一个整套技术栈在系统架构用的应用,以此全方面的扫描自己有哪些盲区还需要补充。而接下来的章节会以各个系列的技术栈中遇到的面试题作为切入点,讲解技术要点,了解技术原理,包括;数据结构、数据算法、技术栈、框架等进行逐步展开学习。

在进入数据结构章节讲解之前可以先了解下,数据结构都有哪些,基本可以包括;数组(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},包括;

  1. HashCode 源码测试验证工程,interview-03
  2. 103976个英语单词库.txt,验证HashCode值
  3. HashCode散列分布.xlsx,散列和碰撞图表

四、源码讲解

1. 固定乘积31在这用到了

  1. // 获取hashCode "abc".hashCode();
  2. public int hashCode() {
  3. int h = hash;
  4. if (h == 0 && value.length > 0) {
  5. char val[] = value;
  6. for (int i = 0; i < value.length; i++) {
  7. h = 31 * h + val[i];
  8. }
  9. hash = h;
  10. }
  11. return h;
  12. }

在获取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》的内容;

  1. 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.

这段内容主要阐述的观点包括;

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

80个赞的回答

  1. 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. 1 a "n.(A)As 或 A's 安(ampere(a) art.一;n.字母A /[军] Analog.Digital,模拟/数字 /(=account of) 帐上"
  2. 2 aaal American Academy of Arts and Letters 美国艺术和文学学会
  3. 3 aachen 亚琛[德意志联邦共和国西部城市]
  4. 4 aacs Airways and Air Communications Service (美国)航路与航空通讯联络处
  5. 5 aah " [军]Armored Artillery Howitzer,装甲榴弹炮;[军]Advanced Attack Helicopter,先进攻击直升机"
  6. 6 aal "ATM Adaptation Layer,ATM适应层"
  7. 7 aapamoor "n.[生]丘泽,高低位镶嵌沼泽"
  • 单词表的文件格式如上,可以自行解析
  • 读取文件的代码比较简单,这里不展示了,可以通过资源下载进行获取

3.2 Hash计算函数

  1. public static Integer hashCode(String str, Integer multiplier) {
  2. int hash = 0;
  3. for (int i = 0; i < str.length(); i++) {
  4. hash = multiplier * hash + str.charAt(i);
  5. }
  6. return hash;
  7. }
  • 这个过程比较简单,与原hash函数对比只是替换了可变参数,用于我们统计不同乘积数的计算结果。

3.3 Hash碰撞概率计算

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

  1. private static RateInfo hashCollisionRate(Integer multiplier, List<Integer> hashCodeList) {
  2. int maxHash = hashCodeList.stream().max(Integer::compareTo).get();
  3. int minHash = hashCodeList.stream().min(Integer::compareTo).get();
  4. int collisionCount = (int) (hashCodeList.size() - hashCodeList.stream().distinct().count());
  5. double collisionRate = (collisionCount * 1.0) / hashCodeList.size();
  6. return new RateInfo(maxHash, minHash, multiplier, collisionCount, collisionRate);
  7. }
  • 这里记录了最大hash和最小hash值,以及最终返回碰撞数量的统计结果。

3.4 单元测试

  1. @Before
  2. public void before() {
  3. "abc".hashCode();
  4. // 读取文件,103976个英语单词库.txt
  5. words = FileUtil.readWordList("E:/itstack/git/github.com/interview/interview-01/103976个英语单词库.txt");
  6. }
  7. @Test
  8. public void test_collisionRate() {
  9. List<RateInfo> rateInfoList = HashCode.collisionRateList(words, 2, 3, 5, 7, 17, 31, 32, 33, 39, 41, 199);
  10. for (RateInfo rate : rateInfoList) {
  11. System.out.println(String.format("乘数 = %4d, 最小Hash = %11d, 最大Hash = %10d, 碰撞数量 =%6d, 碰撞概率 = %.4f%%", rate.getMultiplier(), rate.getMinHash(), rate.getMaxHash(), rate.getCollisionCount(), rate.getCollisionRate() * 100));
  12. }
  13. }
  • 以上先设定读取英文单词表中的10个单词,之后做hash计算。
  • 在hash计算中把单词表传递进去,同时还有乘积数;2, 3, 5, 7, 17, 31, 32, 33, 39, 41, 199,最终返回一个list结果并输出。
  • 这里主要验证同一批单词,对于不同乘积数会有怎么样的hash碰撞结果。

测试结果

  1. 单词数量:103976
  2. 乘数 = 2, 最小Hash = 97, 最大Hash = 1842581979, 碰撞数量 = 60382, 碰撞概率 = 58.0730%
  3. 乘数 = 3, 最小Hash = -2147308825, 最大Hash = 2146995420, 碰撞数量 = 24300, 碰撞概率 = 23.3708%
  4. 乘数 = 5, 最小Hash = -2147091606, 最大Hash = 2147227581, 碰撞数量 = 7994, 碰撞概率 = 7.6883%
  5. 乘数 = 7, 最小Hash = -2147431389, 最大Hash = 2147226363, 碰撞数量 = 3826, 碰撞概率 = 3.6797%
  6. 乘数 = 17, 最小Hash = -2147238638, 最大Hash = 2147101452, 碰撞数量 = 576, 碰撞概率 = 0.5540%
  7. 乘数 = 31, 最小Hash = -2147461248, 最大Hash = 2147444544, 碰撞数量 = 2, 碰撞概率 = 0.0019%
  8. 乘数 = 32, 最小Hash = -2007883634, 最大Hash = 2074238226, 碰撞数量 = 34947, 碰撞概率 = 33.6106%
  9. 乘数 = 33, 最小Hash = -2147469046, 最大Hash = 2147378587, 碰撞数量 = 1, 碰撞概率 = 0.0010%
  10. 乘数 = 39, 最小Hash = -2147463635, 最大Hash = 2147443239, 碰撞数量 = 0, 碰撞概率 = 0.0000%
  11. 乘数 = 41, 最小Hash = -2147423916, 最大Hash = 2147441721, 碰撞数量 = 1, 碰撞概率 = 0.0010%
  12. 乘数 = 199, 最小Hash = -2147459902, 最大Hash = 2147480320, 碰撞数量 = 0, 碰撞概率 = 0.0000%
  13. Process finished with exit code 0

以上就是不同的乘数下的hash碰撞结果图标展示,从这里可以看出如下信息;

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

4. Hash值散列分布

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

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

4.1 哈希值分段存放

  1. public static Map<Integer, Integer> hashArea(List<Integer> hashCodeList) {
  2. Map<Integer, Integer> statistics = new LinkedHashMap<>();
  3. int start = 0;
  4. for (long i = 0x80000000; i <= 0x7fffffff; i += 67108864) {
  5. long min = i;
  6. long max = min + 67108864;
  7. // 筛选出每个格子里的哈希值数量,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
  8. int num = (int) hashCodeList.parallelStream().filter(x -> x >= min && x < max).count();
  9. statistics.put(start++, num);
  10. }
  11. return statistics;
  • 这个过程主要统计int取值范围内,每个哈希值存放到不同格子里的数量。
  • 这里也是使用了java8的新特性语法,统计起来还是比较方便的。

4.2 单元测试

  1. @Test
  2. public void test_hashArea() {
  3. System.out.println(HashCode.hashArea(words, 2).values());
  4. System.out.println(HashCode.hashArea(words, 7).values());
  5. System.out.println(HashCode.hashArea(words, 31).values());
  6. System.out.println(HashCode.hashArea(words, 32).values());
  7. System.out.println(HashCode.hashArea(words, 199).values());
  8. }
  • 这里列出我们要统计的乘数值,每一个乘数下都会有对应的哈希值数量汇总,也就是64个格子里的数量。
  • 最终把这些统计值放入到excel中进行图表化展示。

统计图表

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

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

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

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

文中引用

五、总结

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

六、推荐阅读

面经手册 · 第2篇《数据结构,HashCode为什么使用31作为乘数?》的更多相关文章

  1. JS魔法堂:不完全国际化&本地化手册 之 实战篇

    前言  最近加入到新项目组负责前端技术预研和选型,其中涉及到一个熟悉又陌生的需求--国际化&本地化.熟悉的是之前的项目也玩过,陌生的是之前的实现仅仅停留在"有"的阶段而已. ...

  2. Python入门篇-数据结构堆排序Heap Sort

    Python入门篇-数据结构堆排序Heap Sort 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.堆Heap 堆是一个完全二叉树 每个非叶子结点都要大于或者等于其左右孩子结点 ...

  3. Python入门篇-数据结构树(tree)的遍历

    Python入门篇-数据结构树(tree)的遍历 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.遍历 迭代所有元素一遍. 二.树的遍历 对树中所有元素不重复地访问一遍,也称作扫 ...

  4. Python入门篇-数据结构树(tree)篇

    Python入门篇-数据结构树(tree)篇 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.树概述 1>.树的概念 非线性结构,每个元素可以有多个前躯和后继 树是n(n& ...

  5. 面经手册 · 第16篇《码农会锁,ReentrantLock之公平锁讲解和实现》

    作者:小傅哥 博客:https://bugstack.cn 专题:面经手册 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 Java学多少才能找到工作? 最近经常有小伙伴问我,以为我的经验来看 ...

  6. 面经手册 · 第7篇《ArrayList也这么多知识?一个指定位置插入就把谢飞机面晕了!》

    作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 数据结构是写好代码的基础! 说到数据结构基本包括:数组.链表.队列.红黑树等,但当你 ...

  7. 面经手册 · 第10篇《扫盲java.util.Collections工具包,学习排序、二分、洗牌、旋转算法》

    作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 算法是数据结构的灵魂! 好的算法搭配上合适的数据结构,可以让代码功能大大的提升效率. ...

  8. 小刻也能看懂的Unraid系统使用手册:基础篇

    小刻也能看懂的Unraid系统使用手册 基础篇 Unraid系统简介 Unraid 的本体其实是 Linux,它主要安装在 NAS 和 All in One 服务器上,经常可以在 Linus 的视频里 ...

  9. JS魔法堂:不完全国际化&本地化手册 之 拓展篇

    前言  最近加入到新项目组负责前端技术预研和选型,其中涉及到一个熟悉又陌生的需求--国际化&本地化.熟悉的是之前的项目也玩过,陌生的是之前的实现仅仅停留在"有"的阶段而已. ...

随机推荐

  1. 牛客网Java工程师能力评估

    感觉很奇怪,出的题做完之后感觉自己没学过Java一样,不过凭借一些做题的技巧和一些记忆,正确率百分之50,排名前百分之30多,记录一下这次的题目,方便我以后进行二次复习吧 1.下面有关JVM内存,说法 ...

  2. 什么?你正在学web自动化测试?那这些Selenium的基本操作你了解过吗?

    在自动化测试中,我们都知道是通过定位元素来实现的,那么有时候我们定位元素定位不到是为什么呢? 1.页面出现了iframe 2.出现了新的窗口,没有实现句柄的切换 3.三种等待方式,没有选择其中之一来使 ...

  3. P1469 找筷子

    摘要:有n根(n为奇数)长短不一的筷子,里面可以凑成(n-1)/2双筷子,只剩下一根不能凑对,问那根不能凑对的筷子有多长. 乍听起来好像不难,桶是一个好东西,可是一看数据:对于100%的数据,N< ...

  4. 深入浅出Java并发包—CountDownLauch原理分析 (转载)

    转载地址:http://yhjhappy234.blog.163.com/blog/static/3163283220135875759265/ CountDownLauch是Java并发包中的一个同 ...

  5. tineMCE 踩坑:images_upload_handler

    tineMCE 的官方示例提供了前端上传图片方法 images_upload_handler 的写法. 但官方写的有点问题,上传会报错. 不过修改也很简单: images_upload_handler ...

  6. 【JUnit测试】总结

    什么是Junit? Junit是xUnit的一个子集,在c++,paython,java语言中测试框架的名字都不相同 xUnit是一套基于测试驱动开发的测试框架 其中的断言机制:将程序预期的结果与程序 ...

  7. 从零开始学Electron笔记(七)

    在之前的文章我们介绍了一下Electron中的对话框 Dialog和消息通知 Notification,接下来我们继续说一下Electron中的系统快捷键及应用打包. 全局快捷键模块就是 global ...

  8. WBF交易所如何使用二次验证码/谷歌身份验证器

    一般点账户名——设置——安全设置中开通虚拟MFA两步验证 具体步骤见链接  WBF交易所如何使用二次验证码/谷歌身份验证器 二次验证码小程序于谷歌身份验证器APP的优势 1.无需下载app 2.验证码 ...

  9. Linux中profile和bashrc的区别

    profile主要设置系统环境参数(可类比为Windows的系统环境变量),如$PATH /etc/profile ~/.bash_profile bashrc主要用来设置bash命令,如命令别名,a ...

  10. Python 正则表达式简单了解

    match 从字符串的开始匹配  如果开头不符合要求  就会报错 search  用字符串里的每一个元素  去匹配找的元素 1.匹配单个字符 \d 数字 \D 非数字 . 匹配任意字符 除了\n [] ...