更好的重写equals方法

何时重写Equals方法

若是类具备本身特有的“逻辑相等”概念,并且父类尚未重写 equals 方法以实现指望的行为时,就须要重写 equals 方法。java

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

重写Equals的指望结果

在重写 equals 方法的时候,若是知足了如下任何一个条件,就正是所指望的结果:测试

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

重写Equals的原则

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

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

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

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

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

自反性

通常不会违背这个原则,若是违背了,那么出现的现象会是将该类的实例添加到集合后,调用 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;

相关文章
相关标签/搜索