Item10: 在重写equals方法的时候,遵循通用的约定

一、在满足以下这些场景的时候,不要重写equals方法:

1、每个类的实例都是唯一的,比如类Thread,它的每个实例都只是代表活跃的线程而不是具体的值,使用默认Object实现的equals方法就已经能满足需求了;

2、不需要为类提供“逻辑上相等”。例如:java.util.regex.Pattern类可以重写equals方法来判断两个该类的实例是否表示相同的正则表达式,但是实际的应用场景里,并不会需要用到这种功能的方法;

3、父类已经重写equals方法,而且父类重写的equals方法已经能够满足当前的类,例如Set继承了AbstractSet,List即继承了AbstractList,Map继承了AbstractMap。

4、当前类是私有的,而且你能够确定该类的equals方法永远不会被调用。如果想更加保险一点,可以这样重载equals方法,以保证该方法不会被调用;

    @Override
    public boolean equals(Object obj) {
        throw new AssertionError(); // Method is never called;
    }

二、当一个类有“逻辑上相等”的概念,并且它的父类没有重写equals方法的时候,需要重写equals方法。比如说“value classes”:Integer,String。当程序员调用equals方法去比较两个对象的时候,是想比较它们是否“逻辑上相等”,而不是它们是否代表同一个对象。此时,重写equals方法不仅能满足程序员的要求,而且还能让该类的对象在作为map的key或者set集合的元素的时候,表现出我们预期的行为。

另外,有一种“value classes”不需要重写equals方法:instance control的类,例如,单例模式,枚举类型,对于这些类来说,“逻辑一致性”等价于“标识一致性”。

三、重写equals方法的时候,一定要遵循以下这些约定:

1)自反性(Reflexivity): 对于非空引用x来说,x.equals(x) 一定要为true;

2)对称性(Symmetric):对于非空引用x,y来说,x.equals(y) 有且仅有在y.equals(s)返回true的时候才返回true;

3)传递性(Transitive):对于非空引用x, y, z来说,如果 x.euqlas(y)返回true,y.equals(z)返回true,那么x.equals(z)也必须返回true;

4)一致性(Consistent):对于非空引用x, y来说,如果equals方法使用到的属性没有被改变的话,那么多次调用x.equals(y)要返回相同的结果;

5)对于任意非空引用x,x.equals(null)一定要返回false;

如果违反了以上这些约定,那么你可能会发现你的程序表现的不稳定或者宕了,并且很难去定位是什么原因导致错误;很多的类,包括集合类,依赖对象的equals方法。

什么算是相等的关系?笼统的来说,它是一个操作符:将一组元素划分到其元素与另一个元素等价的分组中。这些分组被称为等价类。从用户的角度来说,有效的equals方法应该能够满足等价的类时能够互相取代的。

四、详细看下上面提到的五个特性:

1)自反性(Reflexivity):一个对象必须等价于它自己,如果在编写程序的时候,违反了这一条规则,那么你会发现在使用集合类添加一个对象之后,调用contains方法来判断集合里面有没有该对象,会返回false。

2)对称性(Symmetry):两个对象是否相等需要保持一致性。不注意的情况下,很容易违背了这一特性;例如下面的代码:

public class CaseInsensitiveString {
    private final String s;
    public CaseInsensitiveString(String s) {
        this.s = Objects.requireNonNull(s);
    }
    @Override
    public boolean equals(Object o) {
        return o instanceof CaseInsensitiveString && ((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
    }

    public static void main(String[] args) {
        CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
        String s = "polish";
        System.out.println(cis.equals(s)); // 返回true
        System.out.println(s.equals(cis)); // 返回false

        // 在集合类里面的示例
        List list = new ArrayList<>();
        list.add(cis);
        System.out.println(list.contains(s)); // 返回false,但是在其他的实现里面,可能
                                              // 返回true或者抛出异常
    }
}

如果你违反了对称性,你可能会不清楚你的程序会表现出怎样的行为。为了避免这个问题,你可以这么改写你的代码:

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

3)传递性(Transitivity):A如果equals B,B equals C,那么,A一定要 equlas C;很容易举一个违反了该约定的代码:一个子类添加了一个新的字段,并且该字段影响了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 (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Point point = (Point) o;
        return x == point.x &&
                y == point.y;
    }
}

ColorPoint类继承了Point类,并且新增了color属性:

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

在这种情况下,应该怎么重写equals方法呢?如果复用父类的equals方法的话,那么会导致不同颜色的ColorPoint对象equals为true,这时不可接受的。你可能会这么重写equals方法:

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

这种写法的问题是:如果比较point对象和一个ColorPoint对象的时候,Point对象的equals方法忽略了color,而ColorPoint对象的equals方法不会忽略,所以前者会返回true,然后后者返回false。这就违背了前面的对称性。
你可能会想到这样重写代码来修复这个问题:

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

这样做,虽然能够满足对称性,但是却不能满足传递性:

        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);  // return true;
        p2.equals(p3); // return true;
        p1.equals(p3); // return false;

而且,上面这样的写法,还有可能导致无限的递归。

那要怎么彻底解决上面所说的问题呢?在面向对象的语言里面,这是一个很基础的问题。没有方法能够继承一个类并添加一个属性之后,还能够遵循重写equals方法的约定,除非你准备放弃面向对象的抽象所带来的优势。

推荐使用组合优先于继承:

下面是实现高质量的equals方法的一些准则:

  1. 首先,使用==来判断两个引用是否指向相同的对象,如果是的话,就return true;
  2. 使用 instanceof 来检查参数是否是正确的类型,如果不是的话,返回false;但有时候,可能会有例外的情况:如果类实现的接口重新定义了equals方法,允许实现了该接口的类之间进行比较,那么正确的类型应该是该接口;
  3. 把参数转换成正确的类型;
  4. 对类里面每一个关键的属性,检验与参数里面的相匹配的属性是否一致。如果全都一致,返回true,否则,返回false;

你可能感兴趣的:(Item10: 在重写equals方法的时候,遵循通用的约定)