返回顶部
首页 > 资讯 > 精选 >String hashCode方法选择数字31作为乘子的原因是什么
  • 669
分享到

String hashCode方法选择数字31作为乘子的原因是什么

2023-06-15 15:06:44 669人浏览 薄情痞子
摘要

本篇内容主要讲解“String hashCode方法选择数字31作为乘子的原因是什么”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“String hashCode方法选择数字31作为乘子的原因是什

本篇内容主要讲解“String hashCode方法选择数字31作为乘子的原因是什么”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“String hashCode方法选择数字31作为乘子的原因是什么”吧!

 1. 背景

某天,我在写代码的时候,无意中点开了 String hashCode 方法。然后大致看了一下 hashCode 的实现,发现并不是很复杂。但是我从源码中发现了一个奇怪的数字,也就是本文的主角31。这个数字居然不是用常量声明的,所以没法从字面意思上推断这个数字的用途。后来带着疑问和好奇心,到网上去找资料查询一下。在看完资料后,默默的感叹了一句,原来是这样啊。那么到底是哪样呢?在接下来章节里,请大家带着好奇心和我揭开数字31的用途之谜。

2. 选择数字31的原因

在详细说明 String hashCode 方法选择数字31的作为乘子的原因之前,我们先来看看 String 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];          }          hhash = h;      }      return h;  }

上面的代码就是 String hashCode 方法的实现,是不是很简单。实际上 hashCode 方法核心的计算逻辑只有三行,也就是代码中的 for 循环。我们可以由上面的 for 循环推导出一个计算公式,hashCode 方法注释中已经给出。如下:

s[0]31^(n-1) + s[1]31^(n-2) + &hellip; + s[n-1]

这里说明一下,上面的 s 数组即源码中的 val 数组,是 String 内部维护的一个 char 类型数组。这里我来简单推导一下这个公式:

假设 n=3  i=0 -> h = 31 * 0 + val[0]  i=1 -> h = 31 * (31 * 0 + val[0]) + val[1]  i=2 -> h = 31 * (31 * (31 * 0 + val[0]) + val[1]) + val[2]         h = 31*31*31*0 + 31*31*val[0] + 31*val[1] + val[2]         h = 31^(n-1)*val[0] + 31^(n-2)*val[1] + val[2]

上面的公式包括公式的推导并不是本文的重点,大家了解了解即可。接下来来说说本文的重点,即选择31的理由。从网上的资料来看,一般有如下两个原因:

第一,31是一个不大不小的质数,是作为 hashCode 乘子的优选质数之一。另外一些相近的质数,比如37、41、43等等,也都是不错的选择。那么为啥偏偏选中了31呢?请看第二个原因。

第二、31可以被 JVM 优化,31 * i = (i << 5) - i。

上面两个原因中,第一个需要解释一下,第二个比较简单,就不说了。下面我来解释第一个理由。一般在设计哈希算法时,会选择一个特殊的质数。至于为啥选择质数,我想应该是可以降低哈希算法的冲突率。至于原因,这个就要问数学家了,我几乎可以忽略的数学水平解释不了这个原因。上面说到,31是一个不大不小的质数,是优选乘子。那为啥同是质数的2和101(或者更大的质数)就不是优选乘子呢,分析如下。

这里先分析质数2。首先,假设 n = 6,然后把质数2和 n 带入上面的计算公式。并仅计算公式中次数最高的那一项,结果是2^5 = 32,是不是很小。所以这里可以断定,当字符串长度不是很长时,用质数2做为乘子算出的哈希值,数值不会很大。也就是说,哈希值会分布在一个较小的数值区间内,分布性不佳,最终可能会导致冲突率上升。

上面说了,质数2做为乘子会导致哈希值分布在一个较小区间内,那么如果用一个较大的大质数101会产生什么样的结果呢?根据上面的分析,我想大家应该可以猜出结果了。就是不用再担心哈希值会分布在一个小的区间内了,因为101^5 = 10,510,100,501。但是要注意的是,这个计算结果太大了。如果用 int 类型表示哈希值,结果会溢出,最终导致数值信息丢失。尽管数值信息丢失并不一定会导致冲突率上升,但是我们暂且先认为质数101(或者更大的质数)也不是很好的选择。最后,我们再来看看质数31的计算结果:31^5 = 28629151,结果值相对于32和10,510,100,501来说。是不是很nice,不大不小。

上面用了比较简陋的数学手段证明了数字31是一个不大不小的质数,是作为 hashCode 乘子的优选质数之一。接下来我会用详细的实验来验证上面的结论,不过在验证前,我们先看看 Stack Overflow 上关于这个问题的讨论,Why does Java's hashCode() in String use 31 as a multiplier?。其中排名第一的答案引用了《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是因为它是一个奇质数,如果选择一个偶数会在乘法运算中产生溢出,导致数值信息丢失,因为乘二相当于移位运算。选择质数的优势并不是特别的明显,但这是一个传统。同时,数字31有一个很好的特性,即乘法运算可以被移位和减法运算取代,来获取更好的性能:31 * i == (i << 5) - i,现代的 Java 虚拟机可以自动的完成这个优化。

排名第二的答案设这样说的:

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.

这段话也翻译一下:

正如 Goodrich 和 Tamassia 指出的那样,如果你对超过 50,000 个英文单词(由两个不同版本的 Unix 字典合并而成)进行 hash code 运算,并使用常数 31, 33, 37, 39 和 41 作为乘子,每个常数算出的哈希值冲突数都小于7个,所以在上面几个常数中,常数 31 被 Java 实现所选用也就不足为奇了。

上面的两个答案完美的解释了 Java 源码中选用数字 31 的原因。接下来,我将针对第二个答案就行验证,请大家继续往下看。

3. 实验及数据可视化

本节,我将使用不同的数字作为乘子,对超过23万个英文单词进行哈希运算,并计算哈希算法的冲突率。同时,我也将针对不同乘子算出的哈希值分布情况进行可视化处理,让大家可以直观的看到数据分布情况。本次实验所使用的数据是 Unix/linux 平台中的英文字典文件,文件路径为 /usr/share/dict/words。

3.1 哈希值冲突率计算

计算哈希算法冲突率并不难,比如可以一次性将所有单词的 hash code 算出,并放入 Set 中去除重复值。之后拿单词数减去 set.size() 即可得出冲突数,有了冲突数,冲突率就可以算出来了。当然,如果使用 jdk8 提供的流式计算 api,则可更方便算出,代码片段如下:

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;  }    public static void calculateConflictRate(Integer multiplier, List<Integer> hashs) {      Comparator<Integer> cp = (x, y) -> x > y ? 1 : (x < y ? -1 : 0);      int maxHash = hashs.stream().max(cp).get();      int minHash = hashs.stream().min(cp).get();      // 计算冲突数及冲突率      int uniqueHashNum = (int) hashs.stream().distinct().count();      int conflictNum = hashs.size() - uniqueHashNum;     double conflictRate = (conflictNum * 1.0) / hashs.size();      System.out.println(String.format("multiplier=%4d, minHash=%11d, maxHash=%10d, conflictNum=%6d, conflictRate=%.4f%%",                  multiplier, minHash, maxHash, conflictNum, conflictRate * 100));  }

结果如下:

String hashCode方法选择数字31作为乘子的原因是什么

从上图可以看出,使用较小的质数做为乘子时,冲突率会很高。尤其是质数2,冲突率达到了 55.14%。同时我们注意观察质数2作为乘子时,哈希值的分布情况。可以看得出来,哈希值分布并不是很广,仅仅分布在了整个哈希空间的正半轴部分,即 0 ~ 231-1。而负半轴 -231 ~ -1,则无分布。这也证明了我们上面断言,即质数2作为乘子时,对于短字符串,生成的哈希值分布性不佳。然后再来看看我们之前所说的 31、37、41 这三个不大不小的质数,表现都不错,冲突数都低于7个。而质数 101 和 199 表现的也很不错,冲突率很低,这也说明哈希值溢出并不一定会导致冲突率上升。但是这两个家伙一言不合就溢出,我们认为他们不是哈希算法的优选乘子。最后我们再来看看 32 和 36 这两个偶数的表现,结果并不好,尤其是 32,冲突率超过了了50%。尽管 36 表现的要好一点,不过和 31,37相比,冲突率还是比较高的。当然并非所有的偶数作为乘子时,冲突率都会比较高,大家有兴趣可以自己验证。

3.2 哈希值分布可视化

上一节分析了不同数字作为乘子时的冲突率情况,这一节来分析一下不同数字作为乘子时,哈希值的分布情况。在详细分析之前,我先说说哈希值可视化的过程。我原本是打算将所有的哈希值用一维散点图进行可视化,但是后来找了一圈,也没找到合适的画图工具。加之后来想了想,一维散点图可能不合适做哈希值可视化,因为这里有超过23万个哈希值。也就意味着会在图上显示超过23万个散点,如果不出意外的话,这23万个散点会聚集的很密,有可能会变成一个大黑块,就失去了可视化的意义了。所以这里选择了另一种可视化效果更好的图表,也就是 excel 中的平滑曲线的二维散点图(下面简称散点曲线图)。当然这里同样没有把23万散点都显示在图表上,太多了。所以在实际绘图过程中,我将哈希空间等分成了64个子区间,并统计每个区间内的哈希值数量。最后将分区编号做为X轴,哈希值数量为Y轴,就绘制出了我想要的二维散点曲线图了。这里举个例子说明一下吧,以第0分区为例。第0分区数值区间是[-2147483648, -2080374784),我们统计落在该数值区间内哈希值的数量,得到 <分区编号, 哈希值数量> 数值对,这样就可以绘图了。分区代码如下:

   public static Map<Integer, Integer> partition(List<Integer> hashs) {      // step = 2^32 / 64 = 2^26      final int step = 67108864;      List<Integer> nums = new ArrayList<>();      Map<Integer, Integer> statistics = new LinkedHashMap<>();      int start = 0;      for (long i = Integer.MIN_VALUE; i <= Integer.MAX_VALUE; i += step) {          final long min = i;         final long max = min + step;          int num = (int) hashs.parallelStream()                  .filter(x -> x >= min && x < max).count();          statistics.put(start++, num);          nums.add(num);      }       // 为了防止计算出错,这里验证一下      int hashNum = nums.stream().reduce((x, y) -> x + y).get();      assert hashNum == hashs.size();      return statistics;  }

本文中的哈希值是用整形表示的,整形的数值区间是 [-2147483648, 2147483647],区间大小为 2^32。所以这里可以将区间等分成64个子区间,每个自子区间大小为 2^26。详细的分区对照表如下:

分区编号分区下限分区上限分区编号分区下限分区上限
0-2147483648-208037478432067108864
1-2080374784-20132659203367108864134217728
2-2013265920-194615705634134217728201326592
3-1946157056-187904819235201326592268435456
4-1879048192-181193932836268435456335544320
5-1811939328-174483046437335544320402653184
6-1744830464-167772160038402653184469762048
7-1677721600-161061273639469762048536870912
8-1610612736-154350387240536870912603979776
9-1543503872-147639500841603979776671088640
10-1476395008-140928614442671088640738197504
11-1409286144-134217728043738197504805306368
12-1342177280-127506841644805306368872415232
13-1275068416-120795955245872415232939524096
14-1207959552-1140850688469395240961006632960
15-1140850688-10737418244710066329601073741824
16-1073741824-10066329604810737418241140850688
17-1006632960-9395240964911408506881207959552
18-939524096-8724152325012079595521275068416
19-872415232-8053063685112750684161342177280
20-805306368-7381975045213421772801409286144
21-738197504-6710886405314092861441476395008
22-671088640-6039797765414763950081543503872
23-603979776-5368709125515435038721610612736
24-536870912-4697620485616106127361677721600
25-469762048-4026531845716777216001744830464
26-402653184-3355443205817448304641811939328
27-335544320-2684354565918119393281879048192
28-268435456-2013265926018790481921946157056
29-201326592-1342177286119461570562013265920
30-134217728-671088646220132659202080374784
31-6710886406320803747842147483648
 

接下来,让我们对照上面的分区表,对数字2、3、17、31、101的散点曲线图进行简单的分析。先从数字2开始,数字2对于的散点曲线图如下:

String hashCode方法选择数字31作为乘子的原因是什么

上面的图还是很一幕了然的,乘子2算出的哈希值几乎全部落在第32分区,也就是 [0, 67108864)数值区间内,落在其他区间内的哈希值数量几乎可以忽略不计。这也就不难解释为什么数字2作为乘子时,算出哈希值的冲突率如此之高的原因了。所以这样的哈希算法要它有何用啊,拖出去斩了吧。接下来看看数字3作为乘子时的表现:

String hashCode方法选择数字31作为乘子的原因是什么

3作为乘子时,算出的哈希值分布情况和2很像,只不过稍微好了那么一点点。从图中可以看出绝大部分的哈希值最终都落在了第32分区里,哈希值的分布性很差。这个也没啥用,拖出去枪毙5分钟吧。在看看数字17的情况怎么样:

String hashCode方法选择数字31作为乘子的原因是什么

数字17作为乘子时的表现,明显比上面两个数字好点了。虽然哈希值在第32分区和第34分区有一定的聚集,但是相比较上面2和3,情况明显好好了很多。除此之外,17作为乘子算出的哈希值在其他区也均有分布,且较为均匀,还算是一个不错的乘子吧。

String hashCode方法选择数字31作为乘子的原因是什么

接下来来看看我们本文的主角31了,31作为乘子算出的哈希值在第33分区有一定的小聚集。不过相比于数字17,主角31的表现又好了一些。首先是哈希值的聚集程度没有17那么严重,其次哈希值在其他区分布的情况也要好于17。总之,选31,准没错啊。

String hashCode方法选择数字31作为乘子的原因是什么

最后再来看看大质数101的表现,不难看出,质数101作为乘子时,算出的哈希值分布情况要好于主角31,有点喧宾夺主的意思。不过不可否认的是,质数101的作为乘子时,哈希值的分布性确实更加均匀。所以如果不在意质数101容易导致数据信息丢失问题,或许其是一个更好的选择。

到此,相信大家对“String hashCode方法选择数字31作为乘子的原因是什么”有了更深的了解,不妨来实际操作一番吧!这里是编程网网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

--结束END--

本文标题: String hashCode方法选择数字31作为乘子的原因是什么

本文链接: https://lsjlt.com/news/280678.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • String hashCode方法选择数字31作为乘子的原因是什么
    本篇内容主要讲解“String hashCode方法选择数字31作为乘子的原因是什么”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“String hashCode方法选择数字31作为乘子的原因是什...
    99+
    2023-06-15
  • Node.js成为Web应用开发最佳选择的原因是什么
    这篇文章主要介绍了Node.js成为Web应用开发最佳选择的原因是什么,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。一项颠覆性的技术进入技术...
    99+
    2024-04-02
  • 大多数人选择美国服务器的原因是什么
    大多数人选择美国服务器的原因是:1、美国服务器带宽大,访问速度快;2、美国服务器处于全球网络中心,其机房环境和员工的技术实力都很强;3、美国服务器配置高于国内主机,稳定性相对好一点。具体内容如下:1、美国服务器带宽大租用美国服务器可以解决网...
    99+
    2024-04-02
  • React不将Vite作为构建应用的首选原因是什么
    这篇文章主要介绍“React不将Vite作为构建应用的首选原因是什么”,在日常操作中,相信很多人在React不将Vite作为构建应用的首选原因是什么问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”React不将...
    99+
    2023-07-05
  • MySQL数据库索引选择使用B+树的原因是什么
    这篇文章主要介绍MySQL数据库索引选择使用B+树的原因是什么,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!一、二叉查找树(1)二叉树简介:二叉查找树也称为有序二叉查找树,满足二叉查...
    99+
    2024-04-02
  • 无数企业会选择微信小程序的原因是什么
    小编给大家分享一下无数企业会选择微信小程序的原因是什么,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!自从腾讯推出新形态的应用模式小程序主要就是想体现“用完即走”的...
    99+
    2023-06-27
  • win10打不了字的原因及解决方法是什么
    Win10打不了字的原因一般有以下几种:1. 键盘故障:可能是键盘连接不稳或键盘损坏导致无法输入文字。解决方法:检查键盘连接是否松动...
    99+
    2023-10-18
    win10
  • 大多数人选择HS机房美国服务器的原因是什么
    选择HS机房美国服务器的原因有:1.访问速度快,接入CN2线路,平均延迟180ms;2.产品多样丰富,可以满足客户不同需求;3.服务器稳定,服务器运行稳定无故障;4.提供优质运维服务,提供7*24小运维服务;具体分析如下:访问速度快HS在美...
    99+
    2024-04-02
  • sql判断是否为数字的方法是什么
    在SQL中,可以使用内置函数ISNUMERIC()来判断一个值是否为数字。该函数返回一个整数,如果值是数字,则返回1,否则返回0。示...
    99+
    2024-04-09
    sql
  • python字符串转化为数字的方法是什么
    Python中可以使用`int()`函数将字符串转化为整数。例如:```pythonnum_str = "123"num = int...
    99+
    2023-09-25
    python
  • MySQL字符串转换为数字的方法是什么
    这篇文章主要讲解了“MySQL字符串转换为数字的方法是什么”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“MySQL字符串转换为数字的方法是什么”吧!方法一:直接 加 0select '...
    99+
    2023-06-26
  • php将数字转为字符串的方法是什么
    这篇文章主要讲解了“php将数字转为字符串的方法是什么”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“php将数字转为字符串的方法是什么”吧!4种方法:1、使用“.”字符将数字变量和空字符拼接...
    99+
    2023-07-05
  • oracle判断为纯数字的方法是什么
    在Oracle中,可以使用REGEXP_LIKE函数来判断一个字符串是否为纯数字。以下是一个例子:SELECT CASE W...
    99+
    2023-08-18
    oracle
  • java判断字符串是否为数字的方法是什么
    Java 判断字符串是否为数字的方法有以下几种:1、使用正则表达式可以使用正则表达式判断字符串是否为数字。例如,使用 \d+ 匹配一...
    99+
    2023-05-13
    java判断字符串 java
  • mongodb数据丢失的原因及解决方法是什么
    MongoDB数据丢失的原因可能有多种,包括硬件故障、网络故障、软件错误、人为操作错误等。以下是一些常见的原因和解决方法: 硬件...
    99+
    2023-10-21
    mongodb
  • Golang函数作为参数使用的方法是什么
    这篇文章主要讲解了“Golang函数作为参数使用的方法是什么”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Golang函数作为参数使用的方法是什么”吧!为什么需要将函数作为参数传递在Gola...
    99+
    2023-07-06
  • 选择云计算数据库的正确方法是什么
    这篇文章将为大家详细讲解有关选择云计算数据库的正确方法是什么,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。近年来,企业将本地部署的数据迁移云端越来越成为一种...
    99+
    2024-04-02
  • python不能赋值给文字的原因及解决方法是什么
    Python中不能直接将字符串赋值给变量的原因是因为Python是一种强类型语言,变量的类型是在运行时自动确定的。字符串是不可变的对...
    99+
    2023-10-25
    python
  • randint函数用不了的原因及解决方法是什么
    randint函数用不了的原因可能是因为没有正确导入random模块。要使用randint函数,需要先导入random模块。解决方法...
    99+
    2023-08-11
    randint
  • mysql_query()函数执行失败的原因及解决方法是什么
    mysql_query()函数执行失败的原因和解决方法可能有以下几种:1. 连接数据库失败:如果连接数据库失败,可以检查数据库服务器...
    99+
    2023-08-08
    mysql_query()
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作