本章是:为什么重写
equals()
方法时一定要重写hashCode()
方法深入解析。
# equals 和 hashCode 是什么
在
Java
开发中每个对象都有一个默认的equals()
方法,它比较的是对象的引用是否相等 (即:比较两个对象是否是同一个实例)。但是在实际开发中,我们通常需要比较对象的内容是否相等,而不仅仅是比较它们的引用。这就是为什么我们要重写equals()
方法的原因。
hashCode(哈希码)
是另一个与对象相关的重要概念,哈希码是一个整数值,它是根据对象的内容计算得出的,在Java
中哈希码主要用于散列数据结构,如:哈希表就是一种常见的数据结构。它可以快速查找存储在其中的对象,哈希码可以帮助我们确定对象在哈希表中的存储位置,从而实现高效的查找操作。
# 为什么要重写 equals 方法
在默认情况下
Java
中的equals()
方法比较的是对象的引用,如果我们不重写equals()
方法,那么对于两个不同的对象即使它们的内容相同,但是当调用equals()
方法时也会返回false
, 因为他们的引用不同,如下示例:
package top.rem.rain.override.equlals; | |
/** | |
* @Author: LightRain | |
* @Description: 学生对象 | |
* @DateTime: 2023-12-15 14:55 | |
* @Version:1.0 | |
**/ | |
public class Student { | |
private String name; | |
private String gender; | |
private int age; | |
public Student(String name, String gender, int age) { | |
this.name = name; | |
this.gender = gender; | |
this.age = age; | |
} | |
} |
package top.rem.rain.override; | |
import org.junit.jupiter.api.Test; | |
import org.springframework.boot.test.context.SpringBootTest; | |
import top.rem.rain.override.equlals.Student; | |
@SpringBootTest | |
class OverrideEqualsHashCodeApplicationTests { | |
@Test | |
void equalsDemo1() { | |
Student student1 = new Student("冈崎", "男", 17); | |
Student student2 = new Student("冈崎", "男", 17); | |
System.out.println("student1.equals(student2) = " + student1.equals(student2)); | |
/* | |
执行结果:student1.equals (student2) = false | |
*/ | |
} | |
} |
在上面这个示例中尽管
student1
和student2
的内容是相同的,但它们是不同的实例对象,因此equals()
方法返回false
,这肯定不是我们想要的结果,为了解决这个问题,我们就需要重写equals()
方法,以便比较对象的内容而不是引用地址。通常我们会在自定义类中重写equals()
方法以实现我们自己的相等性逻辑,比较对象的属性是否相等。
下面是重写后的
Student
类对象,IDEA
的重写快捷键是Alt+Ins
package top.rem.rain.override.equlals; | |
import java.util.Objects; | |
/** | |
* @Author: LightRain | |
* @Description: 学生对象 | |
* @DateTime: 2023-12-15 14:55 | |
* @Version:1.0 | |
**/ | |
public class Student { | |
private String name; | |
private String gender; | |
private int age; | |
public Student(String name, String gender, int age) { | |
this.name = name; | |
this.gender = gender; | |
this.age = age; | |
} | |
@Override | |
public boolean equals(Object o) { | |
if (this == o) return true; | |
if (o == null || getClass() != o.getClass()) return false; | |
Student student = (Student) o; | |
return age == student.age && Objects.equals(name, student.name) && Objects.equals(gender, student.gender); | |
} | |
@Override | |
public int hashCode() { | |
return Objects.hash(name, gender, age); | |
} | |
} |
package top.rem.rain.override; | |
import org.junit.jupiter.api.Test; | |
import org.springframework.boot.test.context.SpringBootTest; | |
import top.rem.rain.override.equlals.Student; | |
@SpringBootTest | |
class OverrideEqualsHashCodeApplicationTests { | |
@Test | |
void equalsDemo1() { | |
Student student1 = new Student("冈崎", "男", 17); | |
Student student2 = new Student("冈崎", "男", 17); | |
System.out.println("student1.equals(student2) = " + student1.equals(student2)); | |
/* | |
执行结果:student1.equals (student2) = true | |
*/ | |
} | |
} |
再次执行后结果就是
true
了,这样才是我们想要的内容比较结果,接下来深入分析一下equals()
实现原理。
# 1️⃣ equals 内部原理
我们都知道
equals()
方法是超类Object
类中的一个基本方法,用于检测一个对象是否与另一个对象相等。而在Object
类中这个方法实际上是判断两个对象是否具有相同的引用,如果有它们就一定会相等,源码如下:
package java.lang; | |
import jdk.internal.vm.annotation.IntrinsicCandidate; | |
public class Object { | |
public boolean equals(Object obj) { | |
return (this == obj); | |
} | |
} |
我们知道所有的对象都拥有标识 (内存地址) 和状态 (数据),同时
==
运算符比较的是两个对象的内存地址,所以说Object
的equals()
方法是比较两个对象的内存地址是否相等,即:object1.equals(object2)
为true
则表示equals1
和equals2
实际上是引用的同一个对象。
# 2️⃣ equals 与 == 的区别
一般都会回答:
qeuals
比较的是对象内容,==
比较的是对象地址。但从前面我们可以知道equals()
方法在Object
中的实现也是间接使用了==
运算符进行比较的,从严格意义上来说,前面的回答并不完全正确,先来看下段示例代码:
package top.rem.rain.override.equlals; | |
/** | |
* @Author: LightRain | |
* @Description: 示例类 | |
* @DateTime: 2023-12-15 15:33 | |
* @Version:1.0 | |
**/ | |
public class People { | |
private String name; | |
private String gender; | |
private int age; | |
public People(String name, String gender, int age) { | |
this.name = name; | |
this.gender = gender; | |
this.age = age; | |
} | |
public static void main(String[] args) { | |
People people1 = new People("古河渚","女",18); | |
People people2 = new People("古河渚","女",18); | |
System.out.println(people1.equals(people2)); | |
System.out.println(people1 == people2); | |
/* | |
执行结果: | |
false | |
false | |
*/ | |
} | |
} |
对于
==
运算符比较两个People
对象返回了false
, 这点我们很容易就能明白,毕竟它们比较的是内存地址,而people1
和people2
是两个不同的实例对象,所以people1
与people2
的内存地址也不一样,现在的问题是我们希望判断实例中的内容是否相等,但equals()
方法结果却返回了false
,当然对于equals()
方法返回了false
我们也知道是怎么回事,因为equals()
方法来自Object
类,而我们并没有重写equals()
方法,调用的必然是Object
类中原始的equals()
方法,根据前面分析我们也知道该原始equals()
方法内部实现使用的是==
运算符,因此我们必须在People
类中重写equals()
方法来实现比较内容,而不是比较内存地址,重写后的示例如下:
package top.rem.rain.override.equlals; | |
import java.util.Objects; | |
/** | |
* @Author: LightRain | |
* @Description: 示例类 | |
* @DateTime: 2023-12-15 15:33 | |
* @Version:1.0 | |
**/ | |
public class People { | |
private String name; | |
private String gender; | |
private int age; | |
@Override | |
public boolean equals(Object o) { | |
if (this == o) return true; | |
if (o == null || getClass() != o.getClass()) return false; | |
People people = (People) o; | |
return age == people.age && Objects.equals(name, people.name) && Objects.equals(gender, people.gender); | |
} | |
public People(String name, String gender, int age) { | |
this.name = name; | |
this.gender = gender; | |
this.age = age; | |
} | |
public static void main(String[] args) { | |
People people1 = new People("古河渚","女",18); | |
People people2 = new People("古河渚","女",18); | |
System.out.println(people1.equals(people2)); | |
System.out.println(people1 == people2); | |
/* | |
执行结果: | |
true | |
false | |
*/ | |
} | |
} |
总结:默认情况下从
Object
类中继承而来的equals()
方法与==
运算符是等价的,比较的是对象的内存地址,但我们可以重写equals()
方法,使其按照我们的需求方式进行比较,而不再是比较内存地址。
# 3️⃣ equals 的重写规则
- 注意:我们在重写
equals()
方法时,需要注意下面几项规则:自反性
:对于任何非null
的引用值x
,x.equals(x)
应该返回true
。对称性
:对于任何非null
的引用值x
与y
, 当且仅当y.equals(x)
返回true
时x.equals(y)
才返回true
。传递性
:对于任何非null
的引用值x
y
z
, 如果y.equals(x)
返回true
和y.equals(z)
返回true
那么x.equals(z)
也应该返回true
。一致性
:对于任何非null
的引用值x
与y
, 假设对象上equals
比较中的信息没有被修改,则多次调用x.equals(y)
始终应该返回true
或始终返回false
。- 对于任何非空引用值
x
,x.equals(null)
应该返回false
。
- 当然在通常情况下,如果只是进行同一个类两个对象的相等比较一般都可以满足以上五点要求,示例代码如下:
package top.rem.rain.override.equlals; | |
import java.util.Objects; | |
/** | |
* @Author: LightRain | |
* @Description: 示例类 | |
* @DateTime: 2023-12-15 15:33 | |
* @Version:1.0 | |
**/ | |
public class People { | |
private String name; | |
private String gender; | |
private int age; | |
@Override | |
public boolean equals(Object o) { | |
if (this == o) { | |
return true; | |
} | |
if (o instanceof People people) { | |
return age == people.age && Objects.equals(name, people.name) && Objects.equals(gender, people.gender); | |
} | |
return false; | |
} | |
public People(String name, String gender, int age) { | |
this.name = name; | |
this.gender = gender; | |
this.age = age; | |
} | |
public static void main(String[] args) { | |
People people1 = new People("古河渚", "女", 18); | |
People people2 = new People("古河渚", "女", 18); | |
People people3 = new People("古河渚", "女", 18); | |
System.out.println("自反性:" + people1.equals(people1)); | |
System.out.println("对称性:" + people1.equals(people2) + " " + people2.equals(people1)); | |
System.out.println("传递性:" + people1.equals(people2) + " " + people2.equals(people3) + " " + people1.equals(people3)); | |
System.out.print("一致性:"); | |
for (int i = 0; i < 50; i++) { | |
if (people1.equals(people2) != people1.equals(people2)) { | |
System.out.print("equals方法没有遵守一致性!"); | |
break; | |
} | |
} | |
System.out.println("equals方法遵守一致性!"); | |
System.out.println("与null比较:" + people1.equals(null)); | |
/* | |
执行结果: | |
自反性:true | |
对称性:true true | |
传递性:true true true | |
一致性:equals 方法遵守一致性! | |
与 null 比较:false | |
*/ | |
} | |
} |
以上结果我看可以看出
equals()
方法在同一个类的两个对象间比较还是很容易理解的。如果是子类与父类混合比较,那么情况就不太简单了,接着来看下面的示例:
package top.rem.rain.override.equlals; | |
import java.util.Objects; | |
/** | |
* @Author: LightRain | |
* @Description: 继承 People | |
* @DateTime: 2023-12-15 16:34 | |
* @Version:1.0 | |
**/ | |
public class FemalePeople extends People { | |
/** | |
* 职业 | |
*/ | |
private String occupation; | |
public FemalePeople(String name, String gender, int age, String occupation) { | |
super(name, gender, age); | |
this.occupation = occupation; | |
} | |
@Override | |
public boolean equals(Object o) { | |
if (this == o) { | |
return true; | |
} | |
if (o instanceof FemalePeople fp) { | |
return super.equals(fp) && occupation == fp.occupation; | |
} | |
return false; | |
} | |
public static void main(String[] args) { | |
People people1 = new People("古河渚", "女", 18); | |
FemalePeople femalePeople1 = new FemalePeople("古河渚", "女", 18, "学生"); | |
FemalePeople femalePeople2 = new FemalePeople("杏", "女", 16, "学生"); | |
System.out.println(people1.equals(femalePeople1)); | |
System.out.println(femalePeople1.equals(people1)); | |
System.out.println(people1.equals(femalePeople2)); | |
/* | |
执行结果: | |
true | |
false | |
false | |
*/ | |
} | |
} |
当然这个结果也是我们意料之中的,因为
FemalePeople
类型是属于People
类型,因此people1.equals(femalePeople1)
肯定为true
, 对于femalePeople1.equals(people1)
返回false
是因为People
类型不一定是FemalePeople
类型 (People
还可以有其它子类),但是如果有这样一个需求,只要FemalePeople
和People
的姓名、性别、年龄一样我们就认为他们两个是相同的,在这样一种情况下的需求:People(父类)
与FemalePeople(子类)
的混合比较就不符合equals()
方法的对称特性了。显然一个返回true
, 一个返回false
, 根据equals
的对称特性此时两次比较都应该返回true
才对,如何修改才符合对称特性呢?造成不符合对称性的原因是因为People
类型不一定是FemalePeople
类型,在这种情况下我们不应该在equals()
方法中直接返回false
,而应该继续使用父类的equals()
方法进行比较,修改后的代码如下:
package top.rem.rain.override.equlals; | |
/** | |
* @Author: LightRain | |
* @Description: 继承 People | |
* @DateTime: 2023-12-15 16:34 | |
* @Version:1.0 | |
**/ | |
public class FemalePeople extends People { | |
/** | |
* 职业 | |
*/ | |
private String occupation; | |
public FemalePeople(String name, String gender, int age, String occupation) { | |
super(name, gender, age); | |
this.occupation = occupation; | |
} | |
@Override | |
public boolean equals(Object o) { | |
if (this == o) { | |
return true; | |
} | |
if (o instanceof FemalePeople fp) { | |
return super.equals(fp) && occupation == fp.occupation; | |
} | |
return super.equals(o); | |
} | |
public static void main(String[] args) { | |
People people1 = new People("古河渚", "女", 18); | |
FemalePeople femalePeople1 = new FemalePeople("古河渚", "女", 18, "学生"); | |
FemalePeople femalePeople2 = new FemalePeople("杏", "女", 16, "学生"); | |
FemalePeople femalePeople3 = new FemalePeople("杏", "女", 16, "老师"); | |
System.out.println(people1.equals(femalePeople1)); | |
System.out.println(femalePeople1.equals(people1)); | |
System.out.println(people1.equals(femalePeople2)); | |
System.out.println(femalePeople2.equals(femalePeople3)); | |
/* | |
执行结果: | |
true | |
true | |
false | |
false | |
*/ | |
} | |
} |
- 虽然上面代码现在符合了对称性,但还是不符合传递性,按我们之前的需求应该是相等,而且也应该符合
equals
的传递性才对,事实上执行结果却不是这样,出现违背传递性根本原因是:- 父类与子类混合比较。
- 子类中声明了新变量,并且在子类
equals()
方法使用了新增的成员变量作为判断对象是否相等的条件。
- 只要满足上面两个条件
equals()
方法的传递性便失效了,下面是重新修改后的代码,这样就解决了传递性。
package top.rem.rain.override.equlals; | |
/** | |
* @Author: LightRain | |
* @Description: 继承 People | |
* @DateTime: 2023-12-15 16:34 | |
* @Version:1.0 | |
**/ | |
public class FemalePeople extends People { | |
/** | |
* 职业 | |
*/ | |
private String occupation; | |
public FemalePeople(String name, String gender, int age, String occupation) { | |
super(name, gender, age); | |
this.occupation = occupation; | |
} | |
@Override | |
public boolean equals(Object o) { | |
if (this == o) { | |
return true; | |
} | |
if (o instanceof FemalePeople fp) { | |
if (super.equals(fp) && occupation == fp.occupation){ | |
return super.equals(fp) && occupation == fp.occupation; | |
}else { | |
return super.equals(o); | |
} | |
} | |
return super.equals(o); | |
} | |
public static void main(String[] args) { | |
People people1 = new People("古河渚", "女", 18); | |
FemalePeople femalePeople1 = new FemalePeople("古河渚", "女", 18, "学生"); | |
FemalePeople femalePeople2 = new FemalePeople("杏", "女", 16, "学生"); | |
FemalePeople femalePeople3 = new FemalePeople("杏", "女", 16, "老师"); | |
FemalePeople femalePeople4 = new FemalePeople("椋", "女", 16, "老师"); | |
System.out.println(people1.equals(femalePeople1)); | |
System.out.println(femalePeople1.equals(people1)); | |
System.out.println(people1.equals(femalePeople2)); | |
System.out.println(femalePeople2.equals(femalePeople3)); | |
System.out.println(femalePeople4.equals(femalePeople2)); | |
/* | |
执行结果: | |
true | |
true | |
false | |
true | |
false | |
*/ | |
} | |
} |
# 为什么要重写 hashCode 方法
在重写了
equals()
方法后为什么还需要重写hashCode()
方法呢?这是因为在使用散列数据结构时,如:哈希表,我们希望相等的对象具有相等的哈希码。在Java
中哈希表使用哈希码来确定存储对象的位置。如果:两个相等的对象具有不同的哈希码,那么它们将被存储在哈希表的不同位置,导致无法正确查找对象。
package top.rem.rain.override.equlals; | |
import java.util.HashSet; | |
import java.util.Objects; | |
import java.util.Set; | |
/** | |
* @Author: LightRain | |
* @Description: 重写 hashCOde Vs 不重写 hashCode | |
* @DateTime: 2023-12-15 19:22 | |
* @Version:1.0 | |
**/ | |
public class Demo { | |
/** | |
* 只重写 equals 方法未重写 hashCode | |
*/ | |
private static class Student1 { | |
private String name; | |
private String gender; | |
private int age; | |
public Student1(String name, String gender, int age) { | |
this.name = name; | |
this.gender = gender; | |
this.age = age; | |
} | |
@Override | |
public boolean equals(Object o) { | |
if (this == o) return true; | |
if (o == null || getClass() != o.getClass()) return false; | |
Student1 student1 = (Student1) o; | |
return age == student1.age && Objects.equals(name, student1.name) && Objects.equals(gender, student1.gender); | |
} | |
} | |
/** | |
* 重写 equals 和 hashCode 方法 | |
*/ | |
private static class Student2 { | |
private String name; | |
private String gender; | |
private int age; | |
public Student2(String name, String gender, int age) { | |
this.name = name; | |
this.gender = gender; | |
this.age = age; | |
} | |
@Override | |
public boolean equals(Object o) { | |
if (this == o) return true; | |
if (o == null || getClass() != o.getClass()) return false; | |
Student2 student2 = (Student2) o; | |
return age == student2.age && Objects.equals(name, student2.name) && Objects.equals(gender, student2.gender); | |
} | |
@Override | |
public int hashCode() { | |
return Objects.hash(name, gender, age); | |
} | |
} | |
public static void main(String[] args) { | |
Student1 student1 = new Student1("冈崎", "男", 17); | |
Student1 student2 = new Student1("冈崎", "男", 17); | |
Set<Student1> set1 = new HashSet<>(); | |
set1.add(student1); | |
System.out.println("只重写了equals方法:" + set1.contains(student2)); | |
System.out.println("--------------------------------------------------------"); | |
Student2 student3 = new Student2("古河渚", "女", 18); | |
Student2 student4 = new Student2("古河渚", "女", 18); | |
Set<Student2> set2 = new HashSet<>(); | |
set2.add(student3); | |
System.out.println("重写了equals和hashCode方法:" + set2.contains(student4)); | |
/* | |
执行方法: | |
只重写了 equals 方法:false | |
-------------------------------------------------------- | |
重写了 equals 和 hashCode 方法:true | |
*/ | |
} | |
} |
在上面代码示例中,
Student1
类中未重写hashCode()
方法,尽管student1
和student2
内容相同,但由于它们具有不同的哈希码,在使用set1.contains(student2)
时返回false
, 这是说明哈希表无法正确定位到student2
。而Student2
类中重写了hashCode()
方法,在使用set2.contains(student4)
时哈希表就可以正确定位到student4
,因为它们两个的哈希码是相同的,这就是为什么我们需要确保重写equals()
方法时为什么也要重写hashCode()
方法的原因,以便它们的哈希码是相同的,这样哈希表就能正确地存储和查找这些对象了。
# 1️⃣ hashCode 的重写规则
- 注意:我们在重写
hashCode()
方法时,需要注意下面几项规则:- 如果:两个对象通过
equals()
相等,那么它们的哈希码必须相等。 hashCode()
方法的计算应该是高效的,避免复杂的计算。hashCode()
方法的结果应该在对象的生命周期内保持不变,如果:一个对象的内容发生了变化,它的哈希码也一个保持不变。- 对于不相等的对象,哈希码尽量不要相等,以提高哈希表的性能。
- 如果:两个对象通过
- 为了遵守这些规则,通常我们可以使用对象的属性来计算哈希码,比如使用属性的哈希码相加或异或来得到对象的哈希码。
总结:为了保证对象的相等性和哈希表的准确性,我们需要在重写
equals()
方法时一起重写hashCode()
方法。这两个方法是密切相关的,它们一起确保对象在使用散列数据结构时能够正确工作。当你在面试中遇到这个问题时,不要忘记强调equals
和hashCode
方法的一致性和性能,以及遵守重写hashCode
方法的规则。这将帮助你深刻理解这个重要的概念,并在实际开发中正确地使用它们。