08-重写 equals 时请遵守通用约定

重写 equals 方法有许多的重写方式会导致错误,所以要么不重写 equals 方法,要么重写时就要尽力遵守通用约定。

可以不重写equals方法的情况

如果不重写equals方法,那该类的每个实例都只与它自身相等,而有时候这就是我们需要的。

1、类的每个实例本质上都是唯一的
对于代表活动实体(例如 Thread),而不是值(Value)的类来说确实如此,Object提供的equals实现对于这些类来说是正确的行为。

2、不关心类是否提供“逻辑相等”的测试功能
有些类我们只关注它提供的功能,而不是实例之间是否相等。比如java.util.Random类,客户一般用它生成随机数,基本不会检测两个Random生成的随机数是否相同,所以对于这些类重写equals方法并没有意义。

3、父类已经重写equals,从父类继承过来的行为对于子类也是合适的
这些在集合框架中比较常见,比如大多数的Set实现都从AbstractSet继承equals实现,List实现从AbstractList继承equals实现,Map实现从AbstractMap继承equals实现。

4、类是私有的或是包级私有的,可以确定它的equals方法永远不会被调用
这种情况下最好重写equals方法,以防它被意外调用,可以在重写equals方法中抛出异常。

@Override
public boolean equals(Object obj) {
    throw new AssertionError();
}

重写equals方法的情景

如果类具有自己特有的“逻辑相等”概念(不同于对象等同概念),而且父类还没有重写equals以实现期望的行为,这时我们就需要重写equals方法,这通常属于”值类”的情形。

值类仅仅是一个表示值的类,例如Integer和Date,程序猿在利用equals方法来比较值对象的引用时,希望知道它们在逻辑上是否相等。而不是想了解它们是否指向同一个对象。


重写equals方法的通用约定

1、自反性(reflexive):对于任何非null的引用值x,x.equals(x)必须返回true。

2、对称性(symmetric):对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true。

3、传递性(transitive):对于任何非null的引用值x,y,z,如果x.equals(y)返回true,并且y.equals(z)返回true,那么x.equals(z)也必须返回true。

4、一致性(consistent):对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致地返回true,或者一致地返回false。

5、对于任何非null的引用值x,x.equals(null)必须返回false。


违反对称性

举例:

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

测试:

public static void main(String[] args) {
    CaseInsensitiveString cis=new CaseInsensitiveString("Java");
    String str="java";

    System.out.println(cis.equals(str));
    System.out.println(str.equals(cis));    
}

输出:

true
false

当调用 cis.equals(str) 时,使用的是CaseInsensitiveString类的equals方法返回true;但是str.equals(cis)调用的是String类的equals方法返回false;String类是不知道cis是什么鬼,更不知道如何和cis进行比较,肯定返回false ,这就不满足对称性了。

解决这个问题的方法,把企图和String互操作的代码从equals中删掉就可以了。

@Override
public boolean equals(Object o) {
    return o instanceof CaseInsensitiveString
            &&((CaseInsensitiveString)o).s.equalsIgnoreCase(s);
}

违反传递性

定义二维整数型Point类:

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

之后由于需求,添加颜色信息,对Point类进行扩展:

public class ColorPoint extends Point {
    private final Color color;

    public ColorPoint(int x, int y, Color color) {
        super(x, y);
        this.color = color;
    }
}

此时如果对ColorPoint调用equals方法,由于其没有重写equals方法,因此直接调用从父类继承过来的equals方法,在比较过程中将忽略颜色信息 ,这样做虽然没有违反equals约定,但是不符合“逻辑相等”的期望,因此为ColorPoint提供equals方法:

@Override
public boolean equals(Object o) {
    if (!(o instanceof ColorPoint)) {
        return false;
    }
    return super.equals(o) && ((ColorPoint) o).color == color;
}

测试:

public static void main(String[] args) {
    Point p = new Point(1, 2);
    ColorPoint cp = new ColorPoint(1, 2, Color.RED);

    System.out.println(p.equals(cp));
    System.out.println(cp.equals(p));
}

当p.equals(cp)时,使用的是Point中的equals方法,没有颜色信息因此返回true;当cp.equals(p)时,使用ColorPoint的equals方法,返回false,不满足对称性,可以通过重构equals的方法修复这个问题:

@Override
public boolean equals(Object o) {
    if (!(o instanceof Point)) {
        return false;
    }
    // 不带颜色的Point,使用Point的equals方法比较
    if (!(o instanceof ColorPoint)) {
        return o.equals(this);
    }
    return super.equals(o) && ((ColorPoint) o).color == color;
}

上述方法终于保证了对称性,我们可以使用以下测试数据来进行测试:

public static void main(String[] args) {
    ColorPoint p1 = new ColorPoint(1, 2, Color.BLUE);
    Point p2 = new Point(1, 2);
    ColorPoint p3 = new ColorPoint(1, 2, Color.RED);

    System.out.println(p1.equals(p2));
    System.out.println(p2.equals(p3));
    System.out.println(p1.equals(p3));
}

当p1.equals(p2)时返回true;p2.equals(p3)时返回true;p1.equals(p3)时返回false,不满足传递性。

这个问题是面向对象语言中关于等价关系的一个基本问题: 无法在扩展可实例化的类的同时,既增加新的值组件,同时又保留equals的约定。

解决这个问题的方法: 面向对象编程中,组合优先于继承 ,现在的ColorPoint类不再继承Point类,而是通过一个引用组合它,重构之后的代码如下:

public class ColorPoint {
    private final Point point;
    private final Color color;

    public ColorPoint(int x, int y, Color color) {
        if (color == null) {
            throw new NullPointerException();
        }
        this.point = new Point(x, y);
        this.color = color;
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof ColorPoint)) {
            return false;
        }
        ColorPoint cp = (ColorPoint) o;
        return cp.point.equals(point) && cp.color.equals(color);
    }
}

java 类库中 java.sql.Timestamp 的 java.util.Date 就违反对称性约定,可以查看文档中的免责声明,这种做法不建议仿效。


非空性测试

很多类使用一个显式的null测试来避免抛出空指针异常:

@Override
public boolean equals(Object o) {
    if (o == null) {
        return false;
    }
    //...
    return false;
}

其实在equals方法中,最终是将待比较对象转换为当前类的实例,以调用方法或访问属性, 这样必须先经过 instanceof ,而如果 instanceof 的第一个参数为null,则不管第二个参数是那种类型都会返回false,这样可以很好地避免空指针异常并且不需要单独地进行null测试。

@Override
public boolean equals(Object o) {
    if (!(o instanceof MyType)) {
        return false;
    }
    MyType mt = (MyType) o;
}

实现高质量equals方法的诀窍

1、使用==操作符检查 参数是否为这个对象的引用。
2、使用 instanceof 操作符检查 参数是否为正确的类型。
3、把参数转换成正确的类型。
4、对于要比较类中的每个关键域,检查参数中的域是否与该对象中对应的域相匹配。
5、编写完equals方法后需要测试是否满足对称性、传递性和一致性。


本条目最后的告诫

1、重写equals时总要重写hashCode
2、不要企图让equals方法过于智能
3、不要将equals声明中的Object对象替换为其他的类型,因为替换后只是重载Object.equals(Object o),而不是重写。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值