Object
类中的equals
方法用于检测一个对象是否等于另一个对象。默认情况下:如果两个对象引用相等,两个对象才相等。对于很多情况,这已经足够了,如:
-
类的每个实例本质上都是唯一的。
-
不关心类是否提供了“逻辑相等(logical equality)”的功能。
-
超类已经覆盖了equals,从超类继承过来的行为对于子类也是合适的。
但如果类具有自己特有的逻辑相等的概念,如判断String
对象是否相等往往是判断字符串是否相同。当调用equals方法时,希望知道他们在逻辑上是否相等,而不是判断它们是否指向同一个对象,这时就必须覆盖equals
和hashCode
方法。
编写equals
方法时,必须遵守通用约定,JavaSE6规范了equals
实现等价关系,即遵守以下五个性质:
-
自反性(reflexive)。对于任何非null的引用值x,x.equals(x) = true
-
对称性(symmetric)。对于任何非null的引用值x和y,当且仅当y.equals(x) = true时,x.equals(y) = true。
-
传递性(transitive)。对于任何非null的引用值x、y和z,如果x.equals(y) = true,并且y.equals(z) = true,那么x.equals(z) = true。
-
一致性(consistent)。对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致的返回true,或者一致的返回false。
-
对于任何非null的引用值x,x.equals(null) = false
实现模板:
public class A{
private int val;
private String name;
@Override
public boolean equals(Object otherObject){
if(this == otherObject) return true;
if(otherObject == null) return false;
if(getClass() != otherObject.getClass()) return false;
A other = (A) otherObject;
return val == other.val
&& Objects.equals(name,other.name);
}
}
若使用instanceof,只需要将if(getClass() != otherObject.getClass()) return false;
修改为
if(!(otherObject instanceof A)) return false;
,同时不需要判断是否为空,因为如果instanceof函数的第一个操作数为null,会返回false。代码实现如下:
public class A {
private int val;
private String name;
@Override
public boolean equals(Object otherObject) {
if (this == otherObject) return true;
if (!(otherObject instanceof A)) return false;
A other = (A) otherObject;
return val == other.val
&& Objects.equals(name, other.name);
}
}
instanceof
方式允许受比较的对象属于子类对象;而getClass
方式则要求两个参数必须是严格同一类才能比较,否则就返回false。下面分别从instanceof
的角度和getClass
的角度说明实际应用中二者的局限性和建议实现方式。
instanceof
instanceof方式可以用于子类之间的比较,但需要注意在父类与子类的比较很可能会违反equals
所规定的原则,如我们定义类A和B,其中B继承A。
class A {
private final String a;
public A(String a) {
this.a = a;
}
@Override
public boolean equals(Object otherObject) {
if (this == otherObject) return true;
if (!(otherObject instanceof A)) return false;
A other = (A) otherObject;
return Objects.equals(a, other.a);
}
}
class B extends A {
private final String b;
public B(String a, String b) {
super(a);
this.b = b;
}
@Override
public boolean equals(Object otherObject) {
if (this == otherObject) return true;
if (!(otherObject instanceof B)) return false;
B other = (B) otherObject;
return super.equals(otherObject) && Objects.equals(b, other.b);
}
}
这时,编写如下测试:
A a = new A("ss");
B b = new B("ss", "bb");
System.out.println(a.equals(b)); // true
System.out.println(b.equals(a)); // false
这是因为前一种比较忽略了字符串b
,而后一种比较则总是返回false,因为参数的类型不正确。当然,你可以尝试在比较时查询受比较对象的类型,如果对象是父类类型则调用父类的equals方法,修改如下:
@Override
public boolean equals(Object otherObject) {
if (this == otherObject) return true;
if (!(otherObject instanceof A)) return false;
if(!(otherObject instanceof B)) return otherObject.equals(this);
B other = (B) otherObject;
return super.equals(otherObject) && Objects.equals(b, other.b);
}
这样对称性将得到保证,但是传递性却无法保证了:
B b = new B("ss", "bb");
A a = new A("ss");
B b2 = new B("ss","cc");
System.out.println(b.equals(a)); // true
System.out.println(a.equals(b2)); // true
System.out.println(b.equals(b2)); // false
此处b.equals(a) = true, a.equals(b2) = true
,但b.equals(b2) = false
,显然传递性无法得到保证。
事实上,这时面向对象程序设计语言中关于等价关系的一个基本问题,即:
我们无法在扩展可实例化的类的同时,既增加新的值组件,又保证equals规定。
这时一些程序员就引入了getClass
判断。
getClass
使用getClass
方法判断就不会出现instanceof
中子类继承父类,父类与子类之间或子类与子类之间进行判断相等的问题,因为它严格要求判断的对象与被判断的对象同属一个类,否则会返回false,这样它显然能满足equals
方法的原则。
但是,《effective Java》中提出,这将违反里氏替换原则(Liskov substitution principle),即一个类型的任何重要属性也将适用于它的子类型,因此为该类型的编写的任何方法,在它的子类型上也应该同样运行的很好。一个典型的例子就是AbstractSet
和其子类TreeSet
和HashSet
,它们采用不同的算法查找集合元素,但我们肯定希望能够比较这两个集合,但如果采取getClass
方法,TreeSet
和HashSet
之间的元素自然不可能相等。
《Java核心技术 卷1》中指出,对于这个问题,解决方法是:应该在AbstractSet
中使用instanceof
定义equals
方法,并将其定义为final
类型,确保子类无法实现该方法,但事实上,Java源码中并未将该方法定义为final
方法,此时,按照equals
方法实现的原则,应该使用getClass
方法,(认为是Java实现的一个问题)。
总结
因此,总结一下,目前有三种情形:
-
如果超类是抽象类型,那么在子类中新的值组件,采取instanceof方式检测是不会违反
equals
的约定的,因为我们无法创建一个抽象类型的实例; -
如果非抽象类的子类可以定义自己的
equals
方法,那么对称性需求要求强制使用getClass
方式检测,这样不可以在不同子类或子类与超类之间进行相等性比较; -
如果子类由超类决定自己的相等性概念,那么可以在超类中使用
instanceof
方式检测,并使用final
修饰,符合里氏替换原则,这样可以在不同子类或子类与超类之间进行相等性比较。
注意,对于第2种情形,《effective Java》的作者认为一种不错的权宜之计是采取聚合(复合)而非继承。这种方式本质上是取消了继承,也就不存在子类/超类了,自然也就不会出现问题,不过我认为这是一种逃避的做法,如果一定要使用继承,还是需要使用getClass
方式进行验证。
编写一个完美的equals
方法的建议(《Java核心技术 卷1》):
-
显式参数命名为otherObject,稍后需要将它强制转换成另一个名为other的变量;
-
检测this与otherObject是否相等;
-
检测otherObject是否为null,如果为null,返回false;
-
比较this与otherObject所在的类,如果equals的语义可以在非抽象类型的子类中变化,就必须使用getClass检测,如果equals的语义在抽象类型的子类中变化,可以使用instanceof检测,如果所有的子类(不管是抽象类还是非抽象类的子类)都具有相同的相等性判断,那么可以使用instanceof检测,且仅在超类中定义
equals
方法,声明为final
表示不允许继承; -
将otherObject强制转换为相应类类型的变量;
-
现在根据相等性概念的要求来比较字段,使用
==
比较基本类型字段,使用Objects.equals
比较对象字段,如果在子类中重新定义equals,就要在其中包含一个super.equals(other)判断。
equals
方法看似简单,但实际上其中却充满着陷阱,例如Java中的TimeStamp
和Date
类,TimeStamp
继承Date
类,且包含自己的equals实现,但Date
类中的equals
方法却采用instanceof
方式进行检测,显然就导致了错误。