浅析 hashcode() 和 equals()
在 Java 中,equals()
的默认实现的是判断两个引用变量是否指向同一内存空间,即这两个引用变量是否是对同一实例的引用。而 hashcode()
默认实现与 native
方法相关,我的猜测是和这两个引用所指对象的内存地址有关,事实上的确是这样。为什么我的猜测会是正确的呢?我们来分析原因。
Java 文档对子类重写这两个方法的要求是:
- 要么两个方法都重写,要么都不重写;
- 如果两个对象的通过
equals
方法比较返回true
,那么这两个方法的hashcode
必须相等; - 如果这两个对象的
hashcode
相等,这两个对象不一定equals
比较后返回true
。
看看 String
的 hashcode()
方法和 equals()
方法:
- hashcode() :
1 | public int hashCode() { |
- equals():
1 | public boolean equals(Object other) { |
对于 String
, 我们可以看到:
equals()
方法的 “关键变量”(比较的依据)是字符串中的每个字符。但是为了程序效率,这个方法一开始并不会逐个比较两个字符串的字符,而是先比较它们长度是否相等,再看它们的hashcode()
是否相等(按照规范,如果两个对象相等,它们的hashcode()
一定相等),最后才出 “杀手锏”,逐个地比较它们的字符。hashcode()
方法返回的是对每个字符的 ascii 码进行加权求和。
从文档的规范和 String
中两个方法的实现我们可以暂时可以推导出这样的结论:hashcode()
方法的返回值一定是通过对 equals()
中的关键变量进行某种函数变换得到的(简单点说就是和关键变量有关),只有这样才能保证 equals() 返回 ture => 关键变量相等 => hashcode() 返回值相等。
对于我们自定义的类,如果要重写这两个方法,应该首先重写 equals()
。因为根据第二点要求,hashcode()
方法的实现应该是建立在 equals()
方法之上的:在重写 equals()
方法之后,我们再去保证对于 euqals()
比较返回 true
的两个对象,如何让它们的 hashcode()
返回值相等。
例如,定义一个 People
类:
1 | public class People { |
如果有这样的定义:对于 People
的两个对象 a
和 b
,如果它们的 name
相等我们就认为这两个对象相等。那么 People
的 equals()
方法就应该是这样实现的:
1 | public boolean equals(Object other) { |
equals()
的 “关键变量” 是 name
,所以 hashcode()
的返回值必须是通过对 name
的某种函数变换得到的。
1 | public int hashcode() { |
按照上面的实现,我们可以保证,如果 a.equals(b) == true
,一定有 a.hashcode() == b.hashcode()
。
如果我们按照下面这样实现 hashcode()
会怎样呢?
1 | public int hashcode() { |
在上面的实现中,hashcode()
的返回值并不和 “关键变量” 有关,我们看看这会导致什么问题:假如有两个人 name
相等,hometown
不相等,那么它们通过 equals()
比较会返回 true
,但是由于 hometown
的不相等会直接导致它们 hometown.hashcode()
的返回值不相等,进而导致它们自己的 hashcode()
返回值不相等,这显然违背了 java 的规范。
现在考虑判断两个人相等另一种定义:如果两个 People
对象的 name
和 hometown
都相同,那么它们相等。
那么对于 equals()
方法来说,中规中矩的实现是这样的:
1 | public boolean equals(Object other) { |
而对于 hashcode()
方法来说,就可以这样实现(不是最好的做法,但是符合 java 规范):
1 | public int hashcode() { |
下面的实现也是可以的(同样不建议这样做):
1 | public int hashcode() { |
或者(推荐的做法):
1 | public int hashcode() { |
但是这样就绝对是错误的(与无关变量 age
有关):
1 | public int hashcode() { |
对于上面这种实现,虽然保证了和关键变量 hometown
有关,但是却引入 age
这个非关键变量(无关变量),我们看看这样做有什么问题:有两个 People
对象,它们的 name
和 hometown
相同,但 age
不相等,那么它们通过 equals()
比较会返回 true
, 但它们 hashcode()
的返回值却不相等,因为 age
的不相等导致 hometown.hashcode() + age
不相等,进而导致了这两个对象 hashcode()
的返回值不相等。
因此对于开头的结论,我们还需要进行完善:hashcode()
方法的返回值一定是通过对 equals()
中的关键变量进行某种函数变换得到的(简单点说就是和关键变量有关),并且不能和非关键变量(无关变量,即 equals()
方法中用不到的变量)有关。
现在回到开头的问题:为什么 hashcode()
方法的默认实现与地址相关?因为在 equals()
的默认实现中,关键变量是两个引用变量所指向的内存地址,因此 hashcode()
方法返回值必须和对象的内存地址有关。
感谢阅读
由于只是说明 hashcode()
和 equals()
的内在联系,因此文章中有的例子并是 hashcode()
和 equals()
的最佳实现(但还是满足 java 规范,对于最佳实现可以参考 《Effective Java》 这本书)。另外如果有什么不对的话,还望大家不吝赐教。
参考书籍:《Effective Java》