良好的Java编码习惯(三)

Item7:覆盖equals时需要遵守通用约定

  在我们日常开发过程中,重写equals是比较常用的,但存在许多不合适的覆盖方式导致错误,最好的避免方法就是不去重写equals。但有时我们的业务又需要建立特定的等价关系,而父类中又没有这种特定的等价关系,我们就要重写equals,我们必须遵守它的通用约定(JAVASE6):

  •   自反性(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所用的信息没有被修改,多次调用equals的返回值都一致;
  •        对于非null的引用x,   x.equals(null)必须返回false。

下面我们逐条展开讲讲:

自反性:要求对象必须等于其自身,不过多赘述,这条都能达到;

对称性:要求两个引用相互对等,下面举个违反对称性的例子:

public final class CaseInsensitiveString {
    private final String s;
    
    public CaseInsensitiveString(Srting 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.equlsIgnoreCase((String) o);
        return false;
    }

}
//实例化
CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s = "polish";
cis.equals(s);
s.equals(cis);

这是一个不区分大小写的字符串类,结果是:true,false。

究其原因,在于String类中的equals方法并不区分大小写,这就违背了对称性,难以预测该对象的行为。

传递性:要求一个对象等于第二的对象,并且第二个对象等于第三个对象,则第一个对象一定等于第三个对象。下面有一个二维点的类作为引例:

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;
    }

    @Override 
    public boolean equals(Object o){
        if(!(o instanceof Point))  //首先判断是否为二维点对象
            return false;
  
        if(!(o instanceof ColorPoint))  //如果是普通二维点,直接调用普通二维点对象的equals方法
            return o.equals(this);
        
        return super.equals(o) && ((ClolorPiont) o).color = color;  //最后是带颜色的二维点的判断
    }
}    

以上的equals方法的确提供了对称性,但牺牲了传递性。观察以下代码:

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);
p1.equals(p3);

显然只有p1.equals(p3)返回false。

这就引出了面向对象语言关于等价关系的一个矛盾:无法在扩展可实例化的类的同时,既增加新的组件,又并保留equals约定。目前还没有什么方法能够完美解决这个矛盾,但是权宜之计还是存在的:利用复合代替继承

public class ColorPoint {
    private final Point point;
    private final Color color;
    public ColorPoint(int x, int y,Color color) {
        point = new Point(x, y);
        this.color = color;
    }

    public boolean equals(Object o){
        if(!(o instanceof ColorPoint)) //参数强制转换前推荐先判断是否为对应类型
            return point;
        ColorPoint cp = (ColorPoint) o;
        return cp.point.equals(point) && cp.color.equals(color);
    }

}

直接在有色二维点类中注入二维点对象即可;

 

 
一致性:要求两个相等的对象在任意时刻都是相等的(留意不可变对象和可变对象的比较,相等的对象永远相等,不相等的对象永远不相等)

非空性:要求传进的对象为null时,永远返回false

  在equals开始进行instanceof的实例判断,当参数为null时,表达式永远都是false,不必单独进行非空判断;

总结编写高质量equals方法的诀窍:

  •  观察需求能否使用==比较来提高性能
  • 使用instanceof来检查参数是否为正确类型
  • 转换参数类型
  • 对于每个关键域检查参数与this是否匹配

猜你喜欢

转载自www.cnblogs.com/Joey44/p/10228963.html