如何正确实现 Java 中的 HashCode

时间:2023-01-15 21:23:37

相等 和 Hash Code

从一般角度来看,Equality 是不错的,但是 hash code 更则具技巧性。如果我们在 hash code上多下点功夫,我们就能了解到 hash code 就是用在细微处去提升性能的。

大部分的数据结构使用equals去检查是否他们包含一个元素。例如:

List<String> list = Arrays.asList("a", "b", "c");
boolean contains = list.contains("b");

这个变量 contains 是true。因为他们是相等的,虽然b的实例化(instance)虽然不完全一样(再说一次,忽略String interning)。

 

将传递给 contains 的实例与每个元素进行比较很浪费时间。还好,整个这类数据结构使用了一种更高效的方法。它不会将请求的实例与每个元素比较,而是使用捷径,找到可能与之相等的实例,然后只比较这几项。

这个捷径就是哈希码——从对象计算出来的一个能代表该对象的整数值。与哈希码相同的实例不必相等,但相等的实例一定有相同的哈希码。(或者说应该有,我们稍后会对这个问题进行简单讨论)。这类的数据结构常常使用这种技术命名,在名称中加入 Hash 以便识别,其中最具代表性的就是 HashMap。

一般情况下它们会这样进行:

  • 添加一个元素的时候,使用它的哈希码来计算存放在内部数组(称为桶)中的位置(序号)。

  • 另一个不等同的元素如果具有相同的哈希码,它会被放在同一个桶中,与原来那个放在一起,比如把它们放在一个列表中。

  • 如果传递一个实例给 contains 方法,会先计算它的哈希码来找到桶,只有同一个桶中的元素需要与这个实例进行比较。

使用这种方法实现 contains 的情况很少,在理想的状态下根本不需要 equals 比较。

将 equals、hashCode 定义在 Object 中。

 
 
 

关于哈希的一些思考

如果把 hashCode 作为一种快捷方式取决于其是否相等,那么只有一件事情我们需要关心:相等的对象应该有一致的哈希码。

这也是为什么,如果我们覆写 equals 方法,就必须创建一个匹配的 hashCode 实现!此外,实现 equal 应该是依据我们的实现而实现的,这可能会导致没有相同的哈希码,因为他们使用的是 Object 的实现。

 

hashCode 约定

引用:

对于 hashCode 的一般约定:

  • 在 Java 应用程序中,任何时候对同一对象多次调用 hashCode 方法,都必须一直返回同样的整数,对它提供的信息也用于对象的相等比较,且不会被修改。这个整数在两次对同一个应用程序的执行不中不需要保持一致。

  • 如果两个对象通过 equals(Object) 方法来比较相等,那么这两个对象的 hashCode 方法必须产生同样的整型结果。

  • 如果两个对象通过 equals(Object) 方法比较结果不等,这两个对象的 hashCode 不必产生同不整型结果。然而,开发者应该了解对不等的对象产生不同的整型结果有助于提高哈希表的性能。

第一条反映了 equals 的一致性。第二条是我们在上面提到的要求。第三条陈述了我们下面要讨论的一个重要细节。

 
 

实现 hashCode

Person.hashCode 有个很简单的实现:

@Override
public int hashCode() {
    return Objects.hash(firstName, lastName);
}

通过计算相关字段的哈希码,再把这些哈希码组合起来得到 person 的哈希码。它们用 Object 的工具函数 hash 来参与计算。

选择字段

然而什么字段才是相关的?这些要求有助于回答这个问题:如果相等的对象必须有相同的哈希码,那么在计算哈希码的时候就不应该使用那些不用于相等性检查的字段。(否则,如果两个对象只有那些字段不同的话,它们会相等但哈希码不同。)

所以用于计算哈希码的那些字段应该是用于相等性比较的那些字段的子集。默认情况下,它们会使用相同的字段,但有几个细节需要考虑。

 
 
 

一致性

第一是一致性要求。它应该经过非常严格的计算。如果有字段产生了变化,哈希码也应该允许变化(对于可变类来说,这往往是不可避免的),依赖哈希的数据结构并未准备应付这种情况。

正如我们在上面看到的那样,哈希码用于确定一个元素的桶,但是如果哈希相关的字段发生变化,并不会立即重新计算哈希码,而且内部的数组也不会更新。

这就意味着,再对一个相等的对象甚至同一个对象的查询会失败!这个数据结构会计算当前的哈希码,这个哈希码与实例存入时的哈希码并不相同,这直接导致找错了桶。

小结:最好不要用可变的字段来计算哈希码!

 
 
 

性能

哈希码可能最终会在每次调用 equals 的时候计算,这可能正好发生在代码中性能极为关键的部分,所以考虑性能是很有意义的。相比之下 equals 的优化空间就非常小。

除非是使用了复杂的算法,或者使用的字段非常非常多,组合他们哈希码的计算成本可以忽略不计,因为这不可避免。但是应该考虑是否所有字段都需要包含在计算中!尤其应该以审视的眼光来看待集合,例如计算列表和集合中所有元素的哈希码。需要根据不同的情况来考虑是否需要它们参与计算。

如果性能是关键,使用 Object.hash 就可能不是最好的选择,因为它会为可变参数创建数组。

一般的优化原则是:谨慎处理!使用一个公共哈希算法的,可能需要放弃集合,并在分析可能的改进之后进行优化。

 
 
 

碰撞

如果只关注性能,下面这个实例怎么样?

@Override
public int hashCode() {
    return 0;
}

毫无疑问,它很快。而且相等的对象会有相同的哈希码,这也让我们觉得不错。还有个亮点,它不涉及可变的字段!

但是,想想我们提到的桶是什么?这种情况下所有实例会被装进同一个桶中!通常这会导致使用一个链表来容纳所有元素,这样的性能太糟糕了——比如,每次执行 contains 都会对列表进行线性扫描。

因此,我们得让每个桶里的内容尽可能的少!一个即使对非常相似的对象计算的哈希码也大不相同的算法,会是一个不错的开始。

如何取得,一定程度上取决于选择的字段。我们用于计算的细节,更多时候是为了生成不同的哈希码。注意,这与我们对性能的想法完全相反。结果很有趣,用太多或者太少字段都会导致性能不佳。

防止碰撞的算法是哈希算法的另一部分。

 
 
 

计算哈希

计算字段的哈希码最简单的办法就是直接调用这个字段的 `hashCode`。可以手工来进行合并。一个公共算法是从任意的某个数开始,让它与另一个数(通常是一个小素数)相乘,再加上一个字段的哈希码,然后重复:

int prime = 31;
int result = 1;
result = prime * result + ((firstName == null) ? 0 : firstName.hashCode());
result = prime * result + ((lastName == null) ? 0 : lastName.hashCode());
return result;

这有可能造成溢出,但这不是什么大问题,因为在 Java 中不会引发异常。

注意,如果输入数据有着特定的模式,最好的哈希算法都可能出现异常频繁的碰撞。举个简单的例子,假设我们用一个点的 x 坐标和 y 坐标来计算哈希。一开始不太糟,直到我们发现这样一条直线上的点:f(x) = -x,这些点的 x + y = 0。就会发生大量的碰撞!

不过话说回来:如果没有分析说明一个公共算法不正确,就大胆的使用。

总结

我们已经了解,计算哈希码就像是把某个事物精简成等价的整数相等的对象必须有相同的哈希码。同时为了保证较好的性能,拥有同一个哈希码的的对象越少越好。

这意味着如果覆写了 equals 就必须覆写 hashCode。

实现 hashCode 时:

  • 与 equals 使用相同的字段(或者是这些字段的子集)。

  • 最好不要包含可变字段。

  • 考虑不去调用集合的 hashCode。

  • 使用公共算法,除非输入数据的模式严重影响这个算法的效果。

记住,hashCode 与效果相关,所以不要在上面浪费过多精力,除非分析结果指出优化的必要性。

 
 
原文:https://www.oschina.net/translate/how-to-implement-javas-hashcode-correctly?lang=chs&p=2

如何正确实现 Java 中的 HashCode的更多相关文章

  1. 浅谈Java中的hashcode方法

    哈希表这个数据结构想必大多数人都不陌生,而且在很多地方都会利用到hash表来提高查找效率.在Java的Object类中有一个方法: 1 public native int hashCode(); 根据 ...

  2. java中的hashcode

    hashcode的作用 对于包含容器类型的程序设计语言来说,基本上都会涉及到hashCode.在Java中也一样,hashCode方法的主要作用是为了配合基于散列的集合一起正常运行,这样的散列集合包括 ...

  3. 千万不要误用 java 中的 HashCode 方法

    刚才debug追堆栈的时候发现一个很奇怪的问题 我用IE8和Google的浏览器访问同一个地址 Action的 scope="session" 也设置了 而且两个浏览器提交的参数m ...

  4. 【转】浅谈Java中的hashcode方法(这个demo可以多看看)

    浅谈Java中的hashcode方法 哈希表这个数据结构想必大多数人都不陌生,而且在很多地方都会利用到hash表来提高查找效率.在Java的Object类中有一个方法: public native i ...

  5. K:java中的hashCode和equals方法

      hashCode和equals方法是Object类的相关方法,而所有的类都是直接或间接的继承于Object类而存在的,为此,所有的类中都存在着hashCode和equals.通过翻看Object类 ...

  6. 【转】浅谈Java中的hashcode方法

    哈希表这个数据结构想必大多数人都不陌生,而且在很多地方都会利用到hash表来提高查找效率.在Java的Object类中有一个方法: public native int hashCode(); 根据这个 ...

  7. Java中的hashcode方法

    一.hashCode方法的作用 对于包含容器类型的程序设计语言来说,基本上都会涉及到hashCode.在Java中也一样,hashCode方法的主要作用是为了配合基于散列的集合一起正常运行,这样的散列 ...

  8. java 中的 hashcode

    在Java的Object类中有一个方法: public native int hashCode(); 根据这个方法的声明可知,该方法返回一个int类型的数值,并且是本地方法,因此在Object类中并没 ...

  9. 浅谈Java中的hashcode方法(转)

    原文链接:http://www.cnblogs.com/dolphin0520/p/3681042.html 浅谈Java中的hashcode方法 哈希表这个数据结构想必大多数人都不陌生,而且在很多地 ...

随机推荐

  1. BZOJ4712 &colon; 洪水

    首先不难列出DP方程: $dp[x]=\min(w[x],h[x])$ $h[x]=\sum dp[son]$ 当$w[x]$增加时,显然$dp[x]$不会减少,那么我们求出$dp[x]$的增量$de ...

  2. D3&period;js 用层画条形图

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title> ...

  3. 获取token之后,再调用匿名方法

    js获取token bpm.api.beginDownload = function (filePath, fileName) { var url = "/Home/GetToken&quo ...

  4. iOS NFC

    #import <CoreNFC/CoreNFC.h> @interface ViewController ()<NFCNDEFReaderSessionDelegate> @ ...

  5. 树莓派B&plus;使用入门&amp&semi;RPI库安装&amp&semi;wringPi库安装

    最近看看试用一下树莓派进行一些开发操作,于是入手一块Raspberry Pi B+的板子来玩.由于没有显示器,没有备用的键盘和鼠标,所以想到用SSH来控制树莓派,刚开始还很担心已经安装好的操作系统到底 ...

  6. 把mysql脚本或其他数据库脚本导入Powerdesigner

    打开powerdesigner,选择File --> Reverse Engineer --> Database…… Model name是模型名称,DBMS选MySQL 5.0 然后确定 ...

  7. 【Python练习题】程序5

    #题目:输入三个整数x,y,z,请把这三个数由小到大输出. # a = input('请输入整数: \n') # # b = input('请输入整数: \n') # # c = input('请输入 ...

  8. Linux如何从零开始搭建nfs服务器(centOS6)

    Server端 1.打印系统版本 cat /etc/redhat-release uname -r uname -m 2.检查是否安装NFS服务 rpm -aq nfs-utils rpcbind L ...

  9. 使用spring遇到问题 事物不提交和更新失败

    1 使用学习使用spring mvc进行前端代码编写,发现提交修改没发sql语句 测试dao层又没问题 解决: 原来是spring配置文件,事物管理 绑定到了dao层.测试界面前端应该绑定到servi ...

  10. 牛客网多校训练第一场 F - Sum of Maximum(容斥原理 &plus; 拉格朗日插值法)

    链接: https://www.nowcoder.com/acm/contest/139/F 题意: 分析: 转载自:http://tokitsukaze.live/2018/07/19/2018ni ...