「equals」「hashCode」

Java 中所有对象都继承Object对象,而Object类中有两个方法:equals()hashCode(),它们是用来判断两个对象是否相等的

本篇文章就刨根问底式的好好总结一下这个知识点!!

内存地址

在正式介绍equals()hashCode()之前,我们先来聊聊「对象的内存地址」

根据 Java 内存区域的划分可知,「对象」都是存储在「堆」上的,那如何正确获取对象内存地址呢?

首先添加依赖:

然后使用addressOf()方法:

注意:大多数 JVM 实现中的内存地址会随着 GC 时移动对象而发生变化

identityHashCode()

关于identityHashCode()VM.current().addressOf()的区别,前者是对象初始地址的哈希值,后者是对象在内存中的实际地址

关于identityHashCode()hashCode()的区别,这里引用 jdk 中的一句话

为什么说identityHashCode()对象初始地址的哈希值呢?

我们发现,GC 前后 the memory address 发生了变化,但是 identityHashCode 没有改变

原因:对象一旦调用了计算哈希的函数,那么哈希值就会被存储在 object header 中,下次直接从 object header 中取即可

equals()

如果我们自定义的类没有重写equals(),那么它就是直接使用Object类的equals(),如下所示:

这样是直接比较两个对象的内存地址,所以在这种情况下,equals()==是等价的!!

下面的问题面试经常被问,根据上面的分析那到底怎么回答才更佳呢?

「equals() 方法」与「== 运算符」有什么区别呢?

hashCode()

hashCode()在「内存地址」部分已经提到过,如果没有重写,它和identityHashCode()返回的值没啥区别

重点在于「重写」,著名问题:为什么重写 equals() 的同时还得重写 hashCode() ?

一个保证可靠性,一个保证性能:

用 String 举例,摘出了关键代码:

所以很明显了,如果所有的对象都直接用 equals 比较,显然很浪费时间。hashCode 只用比较两个整数是否相等,所以很快。先比较一波 hashCode,相等后再用 equals 比较

hashCode 在 Set Map 中的应用

Java 中的SetMap在比较key时,也是采用这种思想,先比较 hashCode,再比较 equals

设想一种场景,当我们使用自定义的对象作为Mapkey时,而正好只重写equals(),没有重写hashCode(),会发生什么情况??

最后和我们设想的结果可能不一致,其实原因也很简单,put时的 key 对象和get时的 key 对象,虽然它们的字段相同,但确是两个不同的对象,hashCode 显然不同,所以导致最后结果为 null

如果想要结果和预期一样,就必须重写hashCode(),让内存地址不同,但字段相同的对象有一样的 hashCode 值

最后,附上阿里巴巴 Java 开发手册上的一段话:

关于 hashCode 和 equals 的处理,遵循如下规则:

  • 只要覆写 equals,就必须覆写 hashCode
  • 因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须覆写这两种方法
  • 如果自定义对象作为 Map 的键,那么必须覆写 hashCode 和 equals

说明:String 因为覆写了 hashCode 和 equals 方法,所以可以愉快地将 String 对象作为 key 来使用

hashCode 生成方式

前文说默认的 hashCode 是对象初始地址的哈希值,其实不太严谨!!

不同的JVM对hashcode值的生成方式不同。Open JDK 中提供了 6 中生成 hash 值的方法

其中在 OpenJDK 6、7 中使用的是随机数生成器的 (第 0 种) 方式,OpenJDK 8、9 则采用第 5 种作为默认的生成方式

所以,单纯从 OpenJDK 的实现来说,其实 hashcode 的生成与对象内存地址没有什么关系。而 Object 类中 hashCode 方法上的注释,很有可能是早期版本中使用到了第 4 种方式

hashCode 的约定

最后,我们来看一下对 hashCode 方法的约定和说明

参考文章