详解Java中的相等测试equals与继承
Object中有一个equals方法,用来测试两个对象是否相等。该方法判断两个对象相等的条件是,两个对象的引用是否相等。如果两个对象的引用相等的话,那么毋庸置疑这两个对象一定相等。
但是,我们经常需要对对象的字段进行比较,如果两个对象具有相同的字段值,就认为这两个对象相等。比如,有两个员工姓名、年龄以及薪水相等的话,就认为他们相等。
下面我们来重写父类的equals方法,不再比较引用,而是比较几个字段值。
package com.studyjava.demo;
import java.util.Objects;
class Employee
{
private String name;
private int age;
private double salary;
public Employee (String name, int age, double salary)
{
this.name = name;
this.age = age;
this.salary = salary;
}
public String getName ()
{
return this.name;
}
public int getAge ()
{
return this.age;
}
public double getSalary ()
{
return this.salary;
}
public boolean equals (Object other)
{
if (this == other) {
return true;
}
if (other == null) {
return false;
}
if (this.getClass() != other.getClass()) {
return false;
}
Employee otherE = (Employee) other;
if (Objects.equals(this.name, otherE.name)
&& this.age == otherE.age
&& this.salary == otherE.salary) {
return true;
} else {
return false;
}
}
}
这里我们刚开始使用了“==”来比较两个对象。补充“==”知识点:
- java中的基本数据类型判断是否相等,直接使用"=="就行了,相等返回true,否则,返回false。
- 但是java中的引用类型的对象比较变态,假设有两个对象obj1,obj2, obj1==obj2 判断是obj1,obj2这两个变量的引用是否相等,即它们所指向的是否为同一个对象(和Object.equals判断方法一致)。
- 在Java API中,有些类重写了equals()方法,它们的比较规则是:当且仅当该equals方法参数不是 null,两个变量的类型、内容都相同,则比较结果为true。这些类包括:String、Double、Float、Long、Integer、Short、Byte、、Boolean、BigDecimal、BigInteger等等,太多太多了,但是常见的就这些了,具体可以查看API中类的equals()方法,就知道了。
好了,现在我们来分析这个重写的equals方法。
- 注意参数的类型必须是Object。如果写成equals(Employee other),则不会覆盖Object类的equals方法,而是定义了一个完全无关的方法。
- 第一个if用了“==”来判断这两个变量是否引用同一对象。如果引用同一变量,那么肯定相等。
- 第二个if用来判断other是否为null,如果为null,那么返回false。这是因为,this肯定不为null,如果为null的话就会抛出异常。
- 第三个条件判断是否同属一个类,如果不是同属一个类,那么就需返回false。
- 接着就需对字段值进行比较了。这里用了Objects.equals()来比较name属性,为什么不直接用this.name.equals(otherE.name)?这是防止this.name如果为null的话会抛出异常。
Object类中的equals方法用于检测一个对象是否等于另一个对象。
if (a.equals(b)) {......}
这里,为了防止a可能为null的情况,可以换成Objects.equals方法。如果a和b都是null,则Objects(a,b)将返回true,若其中一个为null,则返回false,若都不是null,则返回a.equals(b)。
下面,我们把Employee的子类Manager的equals方法也重写下:
package com.studyjava.demo;
class Manager extends Employee
{
private double bonus;
public Manager (String name, int age, double salary)
{
super(name, age, salary);
this.bonus = 0;
}
public double getSalary ()
{
return super.getSalary() + this.bonus;
}
public void setBonus (double bonus)
{
this.bonus = bonus;
}
public boolean equals (Object manager)
{
if (!super.equals(manager)) {
return false;
}
Manager mng = (Manager) manager;
return this.bonus == mng.bonus;
}
}
相等测试与继承
如果a.equals(b),a和b不属于同一个类,但他们之间是派生关系,equals该如何处理呢?这是一个非常值得讨论的话题。争论的核心在于是用getClass()检测是否为同一个类,还是用intanceof检测是否为继承关系。
java官方对equals方法有一个对称性的要求,即a.equals(b)为true,那么b.equals(a)也应该为true。这是非常合理的,因为我们不想在使用equals时还需要考虑用a.equals(b)还是b.equals(a)。
假如在之前的例子中,将Employee中equals方法中的
this.getClass() != other.getClass()
修改为
other instanceof this
那么,如果e.equals(m)返回true,(e为Employee实例,m为Manager实例)。那么为满足对称性,要使m.equals(e)也返回true。这样就导致一个问题,Manager中的equals就收到了限制。该equals方法必须愿意将自己与任何一个Employee对象进行比较,而不考虑经理特有的那部分信息。
关于是使用intanceof还是使用getClass,下面给出最佳实践:
- 如果子类可以由自己的相等性概念,则对称性需求将强制使用getClass检测。
- 如果由超类决定相等性概念,那么就可以使用instanceof检测,这样就可以再不同子类的对象之间进行相等性比较。
在员工和经理的例子中,只要相应的字段相等,就认为两个对象相等。如果两个Manager对象的姓名、年龄以及薪水均相等,而奖金不相等,就认为它们是不同的,因此我们要使用getClass检测。
但如果假设使用员工的ID作为相等性检测标准,并且这个相等性概念适用于所有子类,就可以使用instanceof检测,而且应该将Employee.equals声明为final。