equals方法的覆盖(Override)看起来很简单,但是许多的覆盖方式都是错误的,将导致非常严重的后果。规避这类后果的最简单的方法就是不覆盖equals方法,如果满足以下任一条件,就不需要覆盖equals方法:
- 类的每一个实例本质上都是唯一的。对于大部分非值类(value class)的实例来说,Object类提供的equals实现都是正确的。
- 不关心类是否提供了“逻辑相等(logical equality)”的比较功能。
- 父类已经覆盖了equals方法,且父类的equals实现对于子类也是适用的。
- 类是私有的或者是包级私有的,可以确定其equals方法永远不会被调用。在这种情况下,为了防止equals方法被意外调用,可以覆盖equals方法并抛出错误:
@Override
public boolean equals(Object o) {
throw new AssertionError();
}
equals方法的核心意义是实现了等价关系(equivalence relation),在覆盖equals方法时,必须遵守以下规范:
- 非空性(non-nullity) :对于任何非null的引用值x,x.equals(null)必须返回false。
- 自反性(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)返回的结果就会保持一致。
如果违反了以上五条规范,程序将在某些时候表现不正常,甚至崩溃,而且很难找到失败的根源。没有哪个类是孤立的,一个类的实例通常会被频繁地传递给另一个类的实例。有许多类,包括所有的集合类(collection class)在内,都依赖于传递给他们的对象是否遵守了equals的使用规范。
下面,逐一讨论下这几条规范:
非空性是指所有的对象都必须不等于null,一旦出现对象为空的情况,虽然很难出现o.equals(null)意外返回true的情况,但很可能意外抛出NullPointerException异常。许多类的equals方法通过显示的null判断来防止这种情况:
@Override
public boolean equals(Object o) {
if (null == o)
return false;
...
}
这个判断是不必要的,在使用instanceof操作符检查参数类型时就已经实现了null判断功能:
@Override
public boolean equals(Object o) {
if (! (o instanceof MyType))
return false;
MyType type = (MyType) o;
...
}
自反性是指对象必须等于自身,这一条默认是自动满足的,除非强制进行错误的处理。加入违背了这一条,然后把该类的实例添加到集合类中,该集合的contains方法将查询不到添加的实例。
对称性是指任何两个对象对于“它们是否相等”的判断都必须保持一致,违反这一条规定的情况使比较很容易出现的。例如,下面的类实现了一个不区分大小写的字符串。
public final class CaseInsensitiveString{
private String s;
public CaseInsensitiveString(String s){
if(s == null)
throw new NullPointerException;
this.s = s;
}
@Override
public boolean equals(Object o) {
if (o instanceof CaseInsensitiveString)
return s.equalsIgnoreCase(((CaseInsensitiveString)o).s);
if(o instanceof String)
return s.equalsIgnoreCase((String)o);
return false;
...
}
}
在这个类中,equals方法实现了与普通字符串对象进行互操作,假设有一个不区分大小写的字符串和一个普通字符串:
CaseInsensitiveString cis = new CaseInsensitiveString("Csdn");
String s = "csdn";
这种情况下,cis.equals(s)将返回true,但是s.equals(cis)却返回false,这显然违背了对称性。这是因为String类中的equals方法并不知道CaseInsensitiveString。一旦违反了equals规范,当其他对象面对你的对象时,你完全不知道这些对象的行为会怎样。
传递性是指如果一个对象等于第二个对象,并且第二个对象又等于第三个对象,则第一个对象一定等于第三个对象。当子类增加的信息会影响到equals的比较结果时,就容易违反这一规范,下面以Point类为例进行讲解:
public class Point{
private int x;
private int y;
public Point(int x,int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object o) {
if(!(o instanceof Point))
return false;
Point p = (Point)o;
return p.x == x && p.y ==y;
}
}
现在扩展Point类,添加颜色信息:
public class ColorPoint extends Point{
private final Color color;
public Point(int x,int y,Color color) {
super(x,y);
this.color = color;
}
@Override
public boolean equals(Object o) {
if(!(o instanceof Point))
return false;
if(!(o instanceof ColorPoint))
return o.equals(this);
return super.equals(o)&&((ColorPoint)o).color == color;
}
}
假设,现在有如下三个对象:
ColorPoint p1 = new ColorPoint(1,1,Color.RED);
Point p2 = new Point(1,1);
ColorPoint p3 = new ColorPoint(1,1,Color.BLUE);
此时,p1.equals(p2)和p2.equals(p3)都返回true,但是p1.equals(p3)则是返回false,很显然违反了传递性。这是面向对象语言中关于等价问题的一个基本问题。我们无法在扩展(extends)可实例化的类的同时,既增加新的值组件(value component),同时又保留equals规范。虽然没有一种令人满意的办法可以既扩展可实例化的类,又增加值组件,但是还是有一种选择,即通过复合(composition)的方式实现(本文不再展开)。另外,可以在一个抽象类(abstract class)的子类中增加新的值组件,而不违反equals规范,也就是说,只要可能直接创建父类的实例,前面说的问题就不会发生。
一致性是指对于可变对象,如果两个对象相等,他们就必须始终相等,除非它们中至少有一个对象被修改了。对于不可变对象,就必须保证equals方法满足这样的限制条件:相等的对象永远相等,不相等的对象永远不相等。无论类是否是不可变的,都不要使equals方法依赖于不可靠的资源。如果违反了这一条,要满足一致性的要求就十分困难了。除了极少数的例外情况,equals方法都应该对驻留在内存中的对象执行确定性的计算。
以上,详细地讨论了equals方法的使用规范,现在梳理几条实现高质量equals方法的步骤和诀窍:
- 使用==操作符检查“参数是否为这个对象自身的引用”。
- 使用instanceof操作符检查“参数是否为正确的类型”。
- 把参数转换成正确的类型。
- 对于该类中的每个“关键”域,检查参数中的域是否与该对象中对应的域相匹配。
- 完成equals方法编写后,编写单元测试来检验equals方法是否满足对称性、传递性、一致性(自反性和非空性通常会自动满足)。
- 覆盖equals方法时总要覆盖hashCode方法;
- 不要企图让equals方法过于智能;
- 不要将equals方法声明中的Object对象替换为其他的类型:这样是重载(Overload)了Object.equals方法,并没有实现equals方法的覆盖,增加了代码的复杂性。