Java 经验法则8 复写equals时请遵守通用约定
以下情况大可不必要复写equals:
- 类的每个实例本质上都是唯一的(单例).
- 不关心类是否提供了逻辑相等的测试功能。
- 超类已经覆盖了equals,从超类继承过来的行为对于子类也合适。
- 类是私有的或包级私有的,可以确定它的equals方法永远不会被调用。在此无疑是应该做以下处理以防被意外调用。
@Override
public boolean equals(Object o){
throw new AssertionError();
}
而如果类具有自己特有的逻辑相等概念,不同于对象等同概念,而且超类还没覆盖equals以实现期望行为,这时我们就需要覆盖equals方法。
复写equals必须遵守的通用约定
-
自反性:对于任何非空实例x,x.equals(x)必须返回true; 如果违反这条,然后把该类的实例添加到集合中,该集合的contains方法将果断的告诉你,该集合不包含你刚刚添加的实例。
-
对称性:对于任何非空实例x和y,如果x.equals(y)返回true,那么y.equals(x)也必须返回true。 若无意违反了这条,这种情形倒是不难想象。例如考虑下面的类,它实现了一个区分大小写的字符串。字符串由toString保存,但在比较操作中被忽略。
public final class CaseInsensitiveString{
private final 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;
}
}
假设我们有一个不区分大小写的字符串和一个普通字符串:
CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s = "polish";
正如所料,cis.equals(s) 返回true,但问题在于,虽然CaseInsensitiveString类中的equals方法知道普通的字符串String对象,但是在String类中的equals方法却并不知道不区分到小写的字符串,因此,s.equals(cis)返回false,显然违反了对称性。
为了解决这个问题,只需要把企图与String互操作的这段代码从equals方法中去掉就可以了
@Override
public boolean equals(Object o){
return o instanceof CaseInsensitiveString && ((CaseInsensitivieString) o).s.equalsIgnoreCase(s);
}
- 传递性:对于任何非空实例x,y,z,只要x.equals(y)返回true,并且y.equals(z)也返回true,那么x.equals(z)也必须返回true。
- 举个反面栗子:
public class Point{
private final int x;
private final 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;
}
}
假设你想扩展这个类,为一个点添加颜色信息:
public class ColorPoint extends Point{
private final Color color;
public ColorPoint(int x, int y, Color color){
super(x,y);
this.color = color;
}
}
如果不提供equals方法,而是直接从Point继承过来,在equals做比较时颜色信息就会被忽略。虽然不会违反约定,但是很明显这是无法接受。假设你编写了一个equals方法,只有当它的参数是另一个有色点,并具有相同位置和颜色时才返回true
@Override
public boolean equals(Object o){
if(!(o instanceof ColorPoint))
return false;
return super.equals(o) && ((ColorPoint)o).color == color;
}
这个方法问题在于,你在比较普通点和有色点时,以及相反情形时,可能会得到不同的结果。前一种比较忽略了颜色信息,后一种比较则总返回false,因为参数的类型不正确。这违反了对称性。
Point p = new Point(1,2);
ColorPoint cp = new ColorPoint(1,2,Color.RED);
显然,p.equals(cp)返回true,cp.equals(p)返回falsely。你可以尝试来修正这个问题,让ColorPoint.equals在进行混合比较时忽略颜色信息
@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,2,Color.RED);
Point p2 = new Point(1,2);
ColorPoint p3 = new ColorPoint(1,2,Color.BLUE);
此时,p1.equals(p2)和p2.equals(p3)都返回true ,但是p1.equals(p3)则返回false,显然违反了传递性,前两种比较不考虑颜色信息,而第三站比较则考虑颜色信息。
事实上,这是面向对象语言中关于等价关系的一个基本问题。我们无法在扩展可实例化的类的同时,既增加新的组件,同时又保留equals约定。
比较好的解决方法就是只要不可能直接创建超类实例,前面所述种种问题都不会发生。或者超类和子类不同时实例化并且使用。
-
一致性:对于任何非空实例x和y,只要equals的比较操作在对象中所用信息没有被修改,多次调用x.equals(y)就会一致的返回true,或者一致地返回false。
-
非空性:对于任何非空的实例x,x.equals(null)必须返回false。 许多类的equals方法通过一个显式的null检测来防止这种情况。
@Override
public boolean equals(Object o){
if(o == null)
return false;
...
}
其实这项检测是不必要的。为了检测其参数的同等性,equals方法必须先把参数转成适当的类型,以便可以调用它的访问方法或者属性,在进行转换之前,还必须使用instanceof操作符来判断类型。
@Override
public boolean equals(Object o){
if(!(o instanceof MyType))
return false;
MyType mt = (MyType) o;
...
}
如果漏掉这一步那么有可能会抛出ClassCastException异常。但是如果传递进来的参数为null,那么不管instanceof 哪种类型,都会返回false,所以,可以不用单独的null检测。
高质量equals编写:
- 使用==操作符检查参数是否为这个对象的引用
- 使用instanceof操作符检查参数是否正确地类型。
- 把参数转换成正确类型。
- 对于该类每个关键属性都进行比较。
public class Person{
public int id;
public Person(int id){
this.id = id;
}
@Override
public boolean equals(Object o){
//1.
if(this == o)
return true;
//2.
if(!(o instanceof Person))
return false;
//3.
Person p = (Person) o;
//4.
return p.id == id;
}
}
注意:当包含很多关键属性时,在比较时应该最先比较最有可能不一致的属性,或者开销最低的属性。
编写完成后,应该问自己三个问题,是否对称的,是否传递的,是否一致的。
最后一些告诫:
- 复写equals时总要复写hashCode
- 不要企图让equals方法过于智能。
- 不要将equals声明中的Object对象参数替换成其他的类型。
例如:
//这是错误的,这不是复写了equals,而是重载了equals,在原equals基础上,再提供一个强类型equals方法。
public boolean equals(MyClass o){
...
}
为了避免上面错误,在方法前加上@Override注解就好,系统会给你检查。