equals()以前对它的认知只是知道在比较引用类型的时候用equals(),比较基本类型的时候用==。但是对它的判断原理没做过研究,这篇文章用来探究equals()的作用机制,以及如何去实现一个规范的equals()方法。

Java语言规范要求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引用的对象没有发生变化,反复调用x.equals(y)应该返回同样的结果
  • 对于任意非空引用x,x.equals(null)应该返回false

equals()而言最直接的实现方法就是利用instanceof来判断两个对象引用指向的对象是否来自同一个类,如果指向相同,那equals()自然应该返回true,比如java.util.Date下的equals()就是这样实现的。

但现在我们来假设一个场景Manager继承自Employee

具体实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Employee{
public boolean euqals(Object object){
//满足自反性
if(this == object) return ture;
//如果传入的参数为空返回false
if(object == null) return false;
//如果传入的参数不是Employee或其子类的对象,则返回false
if(!(object instanceof Employee)) return false;

//测试字段是否相等
Employee employee = (Employee) object;
return name.equals(employee.name) && salary == other.salary;
}
}

在定义Managerequals()时,首先调用超类的equals()。如果检测失败就肯定不会相等,如果超类中的字段都相等那就那就比较子类中的字段。

1
2
3
4
5
6
7
public class Manager extends Employee{
public boolean equals(Object object){
if(!super.equals(object)) return false;
Manager manager = (Manager) object;
return bonus == object.bonus;
}
}

调用employee.euqals(manager)假设这两个对象拥有相同的姓名和薪水,如果在Employee.euqals()中用instanceof来检测,这个调用会返回true,根据对称性原则反过来调用manager.equals(employee)也得返回true,不过这是不合规范的因为超类不可以向下转型成子类,所以这种调用会抛出ClassCastException。这就陷入了两难的境地,要么改写Managerequals()方法,让它只去比较和Employee共有的字段,而不去考虑和Manager有关的信息。要么保持现状,这样就违反了对称性。

现在考虑一下getClass()方法,它将返回一个对象所属的类。相较于instanceof它们的区别在于getClass()不关注继承关系,它只返回当前类,而instanceof它认为超类和子类是同一个类。Employee将被改写成如下形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Employee{
public boolean euqals(Object object){
//满足自反性
if(this == object) return ture;
//如果传入的参数为空返回false
if(object == null) return false;
//如果传入的参数不是Employee的对象,则返回false
if(getClass() != object.getClass()) return false;

//测试字段是否相等
Employee employee = (Employee) object;
return name.equals(employee.name) && salary == other.salary;
}
}

但这么做又违反了替换原则(任何超类可以出现的地方,子类一定可以出现)。

综合来看这个问题似乎没有什么两全的答案,要么违反一点原则换来功能性的增强,要么坚持原则,削弱功能性。所以现在可以来做一个总结了

  • 如果子类可以有自己的相等性概念,比如比较具体字段的值来确定是否相等,此时使用getClass()来满足对称性需求
  • 如果超类决定相等性概念,那么就可以使用instanceof来检测,这样可以在不同子类对象之间进行相等性比较

比如现在我们将“相等”定义为:只要对应字段相等,就认为两个对象相等。如果两个Manager的名字,薪水相同,而奖金不同就认为它们是不同的,此时使用getClass()来做判断

或者将“相等”定义为:员工ID相等则相等,那么就应该用instanceof来做判断。

总结一下,写一个比较完备的equals()方法应该遵从以下步骤:

  • 先检测传进来的对象是不是和this相等if(this == object) return true;
  • 检测传进来的对象是不是nullif(object == null) return false;
  • 比较this和object的类,如果equals()的语义可以在子类中做改变就用getClass()检测if(getClass() != object.getClass()) return false;如果所有的子类都具有相同的相等性定义则使用instanceof检测if(!(object instanceof Employee)) return false;
  • 将object强制转换为相应类类型的变量ClassName name = (ClassName) object
  • 根据对相等的定义来比较字段,使用==比较基本字段类型,使用Object.equals()比较字段
  • 如果需要在子类中定义euqals(),就要在其中包含一个super.equals(object)调用