第10条.覆盖equals时请遵守通用约定
Object默认的equals方法会比较对象等同,如果类具有自己特有的“逻辑相等”的概念,我们可以覆盖equals方法。在覆盖equals方法的时候,必须遵守以下通用约定:
1.自反性:对于任何非null的引用值x,x.equals(x)必须返回true。
2.对称性:对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true。
package com.example.ownlearn;
import java.util.Objects;
public final class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s){
this.s = Objects.requireNonNull(s);
}
@Override
public boolean equals(Object obj) {
if(obj instanceof CaseInsensitiveString){
return s.equalsIgnoreCase(((CaseInsensitiveString)obj).s);
}
//重构需要去掉
if(obj instanceof String)
return s.equalsIgnoreCase((String)obj);
return false;
//重构需要去掉
}
}
在这个类中,equals方法的意图非常好,他企图与普通的字符串对象进行互操作。假设我们有一个不区分大小写的字符串和一个普通的字符串:
CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s = "polish";
Boolean a = cis.equals(s);
Boolean b = s.equals(cis);
List<CaseInsensitiveString> list = new ArrayList<>();
list.add(cis);
boolean c = list.contains(s);
a的值为true,b的值为false,因为在Stirng的equals方法中比较的时候并没有区分大小写,这显然是违反对称性的,c在这里碰巧返回了一个false,在其他实现中,有可能为true,甚至抛出异常。为了解决这个问题,我们可以将与String互操作的这段代码从equals方法中去掉就可以了。
3.传递性:对于任何非null的引用值x、y和z,如果x.equals(y)返回true,并且y.equals(z)也返回true,那么x.equals(z)也必须返回true。
子类增加的信息会影响equals的比较结果。
package com.example.ownlearn;
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 obj) {
if(!(obj instanceof Point))
return false;
Point p = (Point)obj;
return p.x == x && p.y == y;
}
}
我们现在有一个描述点的类,重写他的equals方法分别对横坐标、纵坐标进行比较来判断是否相等。
现在由于业务扩展我们有了Point的一个自类,我们对原来的字段进行拓展。
package com.example.ownlearn;
import java.awt.*;
public class ColorPoint extends Point {
private final Color color;
public ColorPoint(int x, int y,Color color) {
super(x, y);
this.color = color;
}
}
那么对于子类的equals方法的实现我们有以下几种做法:
1).直接继承父类的equals方法,这样做是没有问题的,但是并没有对Color进行比较,这样做是有问题的。
2).只有当他的参数是另一个有色点,并且具有相同的位置和颜色时,它才会返回true。
@Override
public boolean equals(Object obj) {
if(!(obj instanceof ColorPoint))
return false;
return super.equals (obj) && ((ColorPoint) obj).color == color;
}
测试:
Point point = new Point(1,2);
ColorPoint colorPoint = new ColorPoint(1,2,Color.BLACK);
boolean a = point.equals(colorPoint);
boolean b = colorPoint.equals(point);
返回的结果是:a 的值为true,b的值为false,很明显这是不符合对称性的。
我们通过修改代码来修正这个问题,让ColorPoint.equals在进行混合比较时忽略颜色信息。
@Override
public boolean equals(Object obj) {
if(!(obj instanceof Point))
return false;
if(!(obj instanceof ColorPoint))
return super.equals (obj);
return super.equals (obj) && ((ColorPoint) obj).color == color;
}
测试:
Point point = new Point(1,2);
ColorPoint colorPoint = new ColorPoint(1,2,Color.BLACK);
ColorPoint colorPoint1 = new ColorPoint(1,2,Color.BLUE);
boolean a = point.equals(colorPoint);
boolean b = point.equals(colorPoint1);
boolean c = colorPoint.equals(colorPoint1);
结果a=true,b=true,c=false。违反了传递性。
我们在无法在扩展实例化的类的同时,既增加新的值组件,同时又保留equals约定,除非愿意放弃面对对象的抽象所带来的优势。
一种不错的权宜之计是,我们不再让ColorPonit扩展Point,而是在ColorPoint加入一个私有的Point域,以及一个公有的视图方法。
@Override
public boolean equals(Object obj) {
if(!(obj instanceof ColorPoint))
return false;
ColorPoint cp = (ColorPoint)obj;
return cp.point.equals(point) && cp.color.equals(color);
}
Java平台类库中,有一些类扩展了可实例化的类,并添加了新的值组件。例如,java.sql.Timestamp对java.util.Data进行了扩展,并增加了nanoseconds域。Timestamp的equals确实违反了对称性,如果Timestamp和Date对象用于同一个集合中,或者以其他方式被混合在一起,则会引起不正确的行为。
注意:可以在一个抽象类的自类中增加新的值组价且不违反equals约定。因为抽象类的实例无法被直接创建。
4.一致性:对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致地返回true,或者一致的返回false。
5.对于任何非null的引用值x,x.equals(null)必须返回false。
为了进行比较,我们需要把参数转换成适当的类型,在进行转换前,必须使用instance操作符,检查其参数的类型是否正确。
if(!(obj instanceof ColorPoint))
return false;
ColorPoint cp = (ColorPoint)obj;
结合以上要求,得出以下实现高质量equals方法的诀窍:
1.使用==操作符检查“参数是否为这个对象的引用”,如果是,返回true。
2.使用instance操作符号检查“参数是否为正确的类型”,如果不是返回false。
3.把参数转换成正确的类型。
4.对于该类中每个“关键”域,检查参数中的域是否与该对象中对应的域相匹配,如果这些测试全部成功,则返回true。
需要注意的是:
1)覆盖equals时总要覆盖hashCode
2)不要企图让equals方法过于智能。
3)不要将equals声明中的Onbject对象替换为其他类型
可以使用Google的开源AutoValue框架,他会自动生成这些方法,不要轻易覆盖equals方法,除非迫不得已。