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 | public class Employee{ |
在定义Manager
的equals()
时,首先调用超类的equals()
。如果检测失败就肯定不会相等,如果超类中的字段都相等那就那就比较子类中的字段。
1 | public class Manager extends Employee{ |
调用employee.euqals(manager)
假设这两个对象拥有相同的姓名和薪水,如果在Employee.euqals()
中用instanceof
来检测,这个调用会返回true,根据对称性原则反过来调用manager.equals(employee)
也得返回true,不过这是不合规范的因为超类不可以向下转型成子类,所以这种调用会抛出ClassCastException
。这就陷入了两难的境地,要么改写Manager
的equals()
方法,让它只去比较和Employee共有的字段,而不去考虑和Manager
有关的信息。要么保持现状,这样就违反了对称性。
现在考虑一下getClass()
方法,它将返回一个对象所属的类。相较于instanceof
它们的区别在于getClass()
不关注继承关系,它只返回当前类,而instanceof
它认为超类和子类是同一个类。Employee
将被改写成如下形式
1 | public class Employee{ |
但这么做又违反了替换原则(任何超类可以出现的地方,子类一定可以出现)。
综合来看这个问题似乎没有什么两全的答案,要么违反一点原则换来功能性的增强,要么坚持原则,削弱功能性。所以现在可以来做一个总结了
- 如果子类可以有自己的相等性概念,比如比较具体字段的值来确定是否相等,此时使用
getClass()
来满足对称性需求 - 如果超类决定相等性概念,那么就可以使用
instanceof
来检测,这样可以在不同子类对象之间进行相等性比较
比如现在我们将“相等”定义为:只要对应字段相等,就认为两个对象相等。如果两个Manager的名字,薪水相同,而奖金不同就认为它们是不同的,此时使用getClass()
来做判断
或者将“相等”定义为:员工ID相等则相等,那么就应该用instanceof
来做判断。
总结一下,写一个比较完备的equals()
方法应该遵从以下步骤:
- 先检测传进来的对象是不是和this相等
if(this == object) return true;
- 检测传进来的对象是不是null
if(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)
调用