更好的重写equals方法

什么时候重写Equals方法

如果类具有自己特有的“逻辑相等”概念,而且父类还没有重写 equals 方法以实现期望的行为时,就需要重写 equals 方法。

这样做还可以使得这个类的实例可以被用作 Map 的 Key,或者 Set 的元素,使 Map 或 Set 表现出预期的行为来。

重写Equals的期望结果

在重写 equals 方法的时候,如果满足了以下任何一个条件,就正是所期望的结果:

  1. 类的每个实例本质上都是唯一的;
  2. 不关心类是否提供了 logical equality 的测试功能;
  3. 父类已经覆盖了 equals 方法,从父类继承过来的行为对于子类也是合适的;
  4. 类是私有的或者是包级私有的,确保它的 equals 方法永远不会被调用时需要重写 equals 方法;

重写Equals的原则

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

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

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

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

非空性(non nullity):对于任何非 null 的引用值 x,x.equals(null) 必须返回 false;

自反性

一般不会违背这个原则,如果违背了,那么出现的现象会是将该类的实例添加到集合后,调用 contains 方法查找这个实例,会得到一个 false。

对称性

对称性简单说是,任何两个对象对于它们是否相等的问题都必须保持一致, x 等于 y 那么 y 也要等于 x。举个违反这个原则的例子:

package test.ch01;

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

在这个类中,equals 要做到与普通的字符串比较时不区分大小写,其问题在于 String 类中的 equals 方法并不知道不区分大小写,因此反过来比较并不成立,违反了对称性。

package test.ch01;

public class Test {

    public static void main(String[] args) {

        CaseInsensitiveString cis = new CaseInsensitiveString("Hello");
        String s = "hello";

        System.out.println(cis.equals(s)); // true
        System.out.println(s.equals(cis)); // false
    }

}

解决这个问题,只需要把企图与 String 互操作的代码从 equals 方法中去掉就可以了:

package test.ch01;

public 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) {
        return o instanceof CaseInsensitiveString && ((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
    }
}
package test.ch01;

public class Test {

    public static void main(String[] args) {

        CaseInsensitiveString cis = new CaseInsensitiveString("Hello");
        String s = "hello";

        CaseInsensitiveString cis1 = new CaseInsensitiveString("hello");

        System.out.println(cis.equals(s)); // false
        System.out.println(s.equals(cis)); // false
        System.out.println(cis.equals(cis1)); // true
    }

}

传递性

传递性要求 x 等于 y,y 等于 z,那么 x 也要等于 z。但是,此处有非常重要的一点,面向对象语言关于等价关系的一个基本问题:

无法在扩展可实例化的类的同时,即增加新的值组件,同时又保留 equals 约定,除非愿意放弃面向对象的抽象所带来的优势。

考虑两个类,它们是继承关系:

package test.ch01;

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;
    }
}
package test.ch01;

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 ColorPoint)) {
            return false;
        }
        return super.equals(o) && ((ColorPoint) o).color == color;
    }

}

如果 ColorPoint 类,不重写 equals,而是直接从 Point 继承过来,那么 ColorPoint 与 Point 比较时会忽略掉颜色。

但是如果按照上面代码,重写 equals,那么会违反一致性:

Point p1 = new Point(1, 2);
ColorPoint cp1 = new ColorPoint(1, 2, Color.RED);
System.out.println(p1.equals(cp1)); // true
System.out.println(cp1.equals(p1)); // false

总之面向对象语言的等价交换关系是一个问题。

里氏替换原则(Liskov substitution principle)认为,一个类型的任何重要属性也将适用于他的子类,因此为该类型编写的任何方法,在它的子类型上也应该同样运行的很好。

虽然没有一种令人满意的办法解决上面的问题,但是还是有一个不错的权宜之计:

package test.ch01;

public class ColorPoint {

    private final Point point;
    private final Color color;

    public ColorPoint(Point point, Color color) {
        this.point = point;
        this.color = color;
    }

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

 使用复合方式代替继承,可以解决上面的问题。

一致性

可变对象在不同的时候可以与不同的对象相等,不可变的对象则不会这样。如果认为它应该是不可变的,就必须保证 equals 方法满足:相等的对象永远相等,不想等的对象永远不相等。

无论类是否是不可变的,都不要使 equals 方法依赖不可靠的资源。

非空性

所有对象都必须不等于 null。

高质量的 Equals 方法

1.使用 == 操作符检查“参数是否为这个对象的引用”,如果比较操作有可能很昂贵,就值得先这么做;

2.使用instanceof检查“参数类型是否正确”;

3.把参数转换成正确的类型,在instanceof后,所以会保证成功;

4.对每个关键域,检查参数中的域是否与该对象中对应的域相匹配,float 和 double需要用 Float.compare 和 Double.compare 比较,集合用 Arrays.equals 比较;

5.对于引用域可以为 null 的情况,为了避免导致 NPE,可以写成 :

(field == o.filed || (field != null && field.equals(o.field)))

6.最先比较最有可能不一致的域,或者是开销低的域,最好是同时满足这两个条件;

7.重写 equals 时总要重写 hashCode;

8.不要让 equals 方法过于智能;

9.不要将 equals 声明中的 Object 对象替换为其他类型;

10.最好用 @Override 注解描述 equals;

猜你喜欢

转载自my.oschina.net/u/2450666/blog/1636179