重写equals方法
相信在每个人都有过重写过java的equals的方法的经历。这篇博文就从以下几个方面说明重写equals方法的原由,与君共进步。
一 为什么要重写equals方法
首先我们了解equals方法的作用是什么?
java的官方解释:
Indicates whether some other object is "equal to" this one.
The equals method implements an equivalence relation on non-null object references:
指示某个其他对象是否“等于”这个对象。
equals方法实现了非空对象引用的等价关系.
也就是说比较两个对象是否相等,在Object类中,这个方法是用来判断两个对象是否具有相同的引用。如果两个对象具有相同的引用,那么他们一定是相等的,看看下面这段代码的运行:
public static void main(String[] args) {
Object o1 = new Object();
Object o2 =o1;
Object o3 = o1;
System.out.println(o3.equals(o2));
}
结果是:
输出是: true
这是因为o2和o3来两个对象都指向了相同的引用o1,所以是true。 但是很多时侯我们可能会有这样的需求。比较两个对象的状态(内容)是否相同。看看下面着段代码:
public class Student {
private String name;
public Student() {
}
public Student(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public static void main(String[] args) {
Student stu1 = new Student("张三");
Student stu2 = new Student("张三");
System.out.println(stu1.equals(stu2));
System.out.println(stu1==stu2);
}
}
结果是:
false
false
分析: 为什么使用equals方法返回的不是true,而是和==相同的结果false呢。这是因为Student这个类并没有重写equals方法,当调用equals方法时,实际上调用的是Object类中的equals方法。看看equals方法的源码:
public boolean equals(Object obj) {
return (this == obj);
}
可以看到equals的底层代码使用来实现的,也就是说在此时的equals方法也是利用来比较的两个对象的内存地址是否相同。
所以上面的代码运行结果都是false。
所以要想实现两个对象的状态(内容)相同,就要重写equals方法
重写student类中的equals方法:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Student)) return false;
Student student = (Student) o;
return
Objects.equals(name, student.name);
}
再次运行:
true
false
可以看到,重写了equals方法实现了两个对象的状态比较。使用equals返回的是true。
重写eqauls方法能实现两个对象的状态(内容)比较。
二 重写equals方法的原则是什么
看官方文档给出的解释
- It is reflexive: for any non-null reference value x, x.equals(x) should return true.
- It is symmetric: for any non-null reference values x and y, x.equals(y) should return true if and only if y.equals(x) returns true.
- It is transitive: for any non-null reference values x, y, and z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should return true.
- It is consistent: for any non-null reference values x and y, multiple invocations of x.equals(y) consistently return true or consistently return false, provided no information used in equals comparisons on the objects is modified.
- For any non-null reference value x, x.equals(null) should return false.
也就是,重写equals方法时,必须满足下面原则
- 自反性:对于任何非空参考值x,x.equals(x)应该返回true。
- 对称性:对于任何非空参考值x和y,当且仅当y.equals(x)返回true时,x.equals(y)才应返回true。
- 传递性:对于x,y和z的任何非空引用值,如果x.equals(y)返回true,而y.equals(z)返回true,则x.equals(z)应该返回true。
- 一致性:对于任何非空引用值x和y,只要未修改对象的equals比较中使用的信息,对x.equals(y)的多次调用将始终返回true或始终返回false。
- 对于任何非null参考值x,x.equals(null)应该返回false。
一般而言,对于一个类的两个对象,这五个原则都可以满足。看下面的代码:
public static void main(String[] args) {
Student stu = null;
Student stu1 = new Student("张三");
Student stu2 = new Student("张三");
Student stu3 = new Student("张三");
System.out.println("自反性:"+stu1.equals(stu1));
System.out.println("对称性");
System.out.println(stu1.equals(stu2));
System.out.println(stu2.equals(stu1));
System.out.println("传递性");
System.out.println(stu1.equals(stu2));
System.out.println(stu2.equals(stu3));
System.out.println(stu1.equals(stu3));
System.out.println("一致性:");
for(int i =0; i<5;i++){
if(stu1.equals(stu2)!=stu1.equals(stu2)){
System.out.println("没有遵守一致性");
break;
}
}
System.out.println("遵守了一致性");
System.out.println("非空性");
// 这里的stu对象为null,所以返回false
System.out.println(stu1.equals(stu));
}
输出结果:
自反性:true
对称性
true
true
传递性
true
true
true
一致性:
遵守了一致性
非空性
false
测试这几个原则都能满足
如果一个类中含有继承关系,equals方法是否满足呢。我们让student类继承person类。看下面的代码
public class Person {
private int age;
public Person(){}
public Person(int age) {
this.age = age;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
// 实现父类equals的方法比较。
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person person = (Person) o;
return age == person.age;
}
@Override
public int hashCode() {
return Objects.hash(age);
}
}
让student继承person,并且有自己的独有的属性name。
public class Student extends Person {
private String name;
public Student() {
}
public Student(String name){
this.name = name;
}
public Student(int batch,String name) {
super(batch);
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Student)) return false;
if (!super.equals(o)) return false;
Student student = (Student) o;
return Objects.equals(name, student.name);
}
测试:
public static void main(String[] args) {
Person per1 = new Person(12);
Student stu2 = new Student(12,"张三");
System.out.println(per1.equals(stu2));
System.out.println(stu2.equals(per1));
}
运行结果:
true
false
为什么出现了false呢,这是因为违背了对称性的原则了。分析一下也不难理解,因为student都是person,所以,per1.equals(stu2)是true,但是反过来就不成立,你不能说所有person都是student,所以stu2.equals(per1)是不成立的。这里也要明确一下父类和字类的equals方法的调用关系。在子类中定义equals方法时,首先调用父类的equals方法,如果检测失败,对象就不可能相等,如果超类中的域都相等,才需要比较子类的实例域。那么上述这个问题怎么解决呢,明白了equals方法的调用关系,那就只需在子类中的eqauls方法中添加一个判断。
// 修改student类中的equals方法
@Override
public boolean equals(Object o) {
if (this == o) return true;
// 记得注释这里
//if (!(o instanceof Student)) return false;
if (!super.equals(o)) return false;
if(o instanceof Student){
Student student = (Student) o;
return Objects.equals(name, student.name);
}
// 如果o不是student还需要再判断o 是不是person对象
return super.equals(o);
}
再次运行上述代码:
true
true
就符合对称性了。
三 重写了euquals方法为什么还要重写hashcode呢
看看hashcode的定义:
java官方档对hashcode()方法的解释:
Returns a hash code value for the object. This method is supported for the benefit of hash tables such as those provided by HashMap.
返回对象的哈希码值。支持此方法的好处是可以使用HashMap提供的散列表。
官方文档明确的说明了此方法的好处是可以使用HashMap提供的散列表。这是因为Map接口的类会使用到键对象的哈希码,当我们调用put方法时,就是根据对象的哈希码来计算存储位置的,因此必须提供对哈希码正确的保证。在java中,可以使用hashCode()这个方法来获取对象的哈希值。
public static void main(String[] args) {
String s1 = "hello";
String s2 = "world";
System.out.println(s1.hashCode());
System.out.println(s2.hashCode());
}
输出结果:
99162322
113318802
可以看到不同的对象的hashcode值是不同的。
看看javaAPI的对hashcode的几点说明:
-
在Java应用程序的执行过程中,无论何时在同一个对象上多次调用它,hashCode方法都必须一致地返回相同的整数,前提是不对对象上的equals比较中使用的信息进行修改。此整数不需要在应用程序的一次执行与同一应用程序的另一次执行之间保持一致。
-
如果根据equals(Object)方法,两个对象是相等的,那么在每个对象上调用hashCode方法必须产生相同的整数结果。
-
如果两个对象根据equals(java.lang.Object)方法是不相等的,那么在每个对象上调用hashCode方法必须产生不同的整数结果,这是不需要的。但是,程序员应该意识到,为不相等的对象生成不同的整数结果可能会提高哈希表的性能。
回到我们前面的问题,为什么重写equals方法,必须要重写hashcode方法。在Object类中,hashCode方法是通过Object对象的地址计算出来的,因为Object对象只与自身相等,所以同一个对象的地址总是相等的,计算取得的哈希码也必然相等。对于不同的对象,由于地址不同,所获取的哈希码自然也不会相等。所以如果一个类重写了equals方法,但没有重写hashCode方法,将会直接违法了第2条规定。还有一点就是重写hashcode()方法,可以方便用户将对象插入到散列表中。
四 如何写好一个equals方法
1. 显式参数命名为otherObject,稍后需要将它转换成另一个叫做other的变量。 2. 检测this与otherObject是否引用同一个对象:if(this=otherObject)returntrue;这条语句只是一个优化。实际上,这是一种经常采用的形式。因为计算这个等式要比一个一个地比较类中的域所付出的代价小得多。 3. 检测otherObject是否为null,如果为null,返回false。这项检测是很必要的。if(otherObject=null)returnfalse; 4. 比较this与otherObject是否属于同一个类。如果equals的语义在每个子类中有所改变,就使用getClass检测:if(getClass()!=otherObject.getCIassO)returnfalse;如果所有的子类都拥有统一的语义,就使用instanceof检测:if(!(otherObjectinstanceofClassName))returnfalse; 5. 将otherObject转换为相应的类类型变量:ClassNameother=(ClassName)otherObject 6. 现在开始对所有需要比较的域进行比较了。使用=比较基本类型域,使用equals比较对象域。如果所有的域都匹配,就返回true;否则返回false。returnfieldl==other.field&&Objects.equa1s(fie1d2,other.field2) 7. 如果在子类中重新定义equals,就要在其中包含调用super.equals(other)。如果你用的是eclipse或者是idea集成环境的话,可以使用快捷键自动生成符合以上标准的equals()方法,但是有时候需要自定义,所以知道原理很重要。
追本溯源,方能阔步前行
参考书籍:
1. Java核心技术 第一卷:基础知识
2. java官方文档