美文网首页
重写equals方法时遵守通用约定

重写equals方法时遵守通用约定

作者: hello_kd | 来源:发表于2019-02-24 22:05 被阅读2次

equals方法在Object类提供的,子类在需要的时候可以重写它。那么,什么情况下需要重写、什么情况下不要重写以及重写时需要遵守的约定。下面是阅读effective Java英文版第三版的条款10而总结的。

无需重写

  1. 类的每个实例都是唯一的。比如Thread类,每个Thread实例表示的是一个活动实体而不是具体的值。
  2. 类无需提供“逻辑相等”的功能。比如Java.util.Pattern可以重写equals方法来判断两个pattern实例是否表示相同的正则表达式,但是设计者不认为客户端需要这种功能,因此没有实现它。
  3. 父类已经重写equals方法,并且适用于子类。比如Set的equals方法继承于AbstractSet,List继承于AbstractList,Map继承于AbstractMap
  4. 类的访问级别为私有的或包私有的,且equals方法永远不会被调用。如果你非常谨慎,可以重写equals方法确保equals不会被突然调用,比如:
@Override public boolean equals(Object o) {
    throw new AssertionError(); // Method is never called
}

需要重写

那么什么情况下需要重写呢?当一个类的多个实例间的比较是根据值,而不是判断是否为同一个对象,且其父类没有重写equals方法,这时候需要重写equals方法,这时称这种类为“值类”。比如Integer和String,通常根据equals方法来判断是否值相等,而不是判断是否指向同一个对象。特别是在对象作为Map的key,或者Set的元素时。
有一种特殊的情况下,这种“值类”无需重写。就是单实例类,无论类生成多少个实例,始终指向同一个对象,此时无需重写equals。比如枚举类。

通用约定

自反性reflexive

对于任何的非空引用x,x.equals(x)必须返回true
这个约定很难无意间去打破,比如你增加一个实例到集合中,然后调用集合的contains方法判断实例是否存在集合中,方法返回false。这种基本不会出现。

对称性symmetric

对于任何的非空引用x、y,x.equals(y)返回true当且仅当y.equals(x)返回true
这个约定在一定的情况下可以打破,比如看下面的一个例子

public final class CaseInsensitiveString {
    private final String s;

    public CaseInsensitiveString(String s) {
        this.s = Objects.requireNonNull(s);
    }

    // Broken - violates symmetry!
    @Override
    public boolean equals(Object o) {
        if (o instanceof CaseInsensitiveString)
            return s.equalsIgnoreCase(
                    ((CaseInsensitiveString) o).s);
        if (o instanceof String) // One-way interoperability!
            return s.equalsIgnoreCase((String) o);
        return false;
    }
    // Remainder omitted
}

下面有两个对象:
CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s = "polish";
很明显的,cis.equals(s)返回true,而s.equals(cis)返回false。这就打破了对称性

传递性transitivity

对于任何的非空引用x、y、z,如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)必须返回true
同样的,这个约定在一定情况下也可以被打破,看下面的例子:

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

这时候ColorPoint类如何实现equals方法?如果直接继承于Point类的,那么不会打破equals的约定,但是这很明显不能这样做。假设按下面的方式重写equals方法,也就是待比较的ColorPoint实例的位置和颜色都相等时才返回true

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

但是这个方式有问题的,当用Point和一个ColorPoint比较时返回true,反过来,用ColorPoint和Point比较时返回false,这违反了对称性。比如
Point p = new Point(1, 2);
ColorPoint cp = new ColorPoint(1, 2, Color.RED);
我们可能通过以下的方式来解决上述问题:

 @Override
    public boolean equals(Object o) {
        if (!(o instanceof Point))
            return false;
// If o is a normal Point, do a color-blind comparison
        if (!(o instanceof ColorPoint))
            return o.equals(this);
// o is a ColorPoint; do a full comparison
        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)返回true,p2.equals(p3)返回true,但是p1.equals(p3)却返回false,打破了传递性的约定。
总结,继承一个可实例化类并增加一个值组件(成员变量)是无法再遵守equals的约定,除非放弃使用面向对象的抽象特性,或者是多态特性。但如果父类是抽象的,就不会打破equals约定。

一致性consistent

对于任何非空引用x、y,多次调用x.equals(y)必须始终返回true或始终返回false
这句话的意思是,如果两个对象是相等的,那么它们将始终保持相等,除非其中的一个或者两者都发生了改变。换句话说,可变对象在不同的时间可以是不同的对象,而不可变对象不能。在创建一个类时,需要考虑它是否不可变,如果是,那么就要确保equals方法遵守约定,相等的对象始终保持相等,不相等的对象始终保持不相等。
无论一个类是否为不可变对象,它的equals方法不要依赖于不可靠的资源。比如java.net.URL的equals方法依赖于URL关联主机的IP地址是否相等,转换主机名称到IP地址需要访问网络,并且多次转换不一定保证相同的结果,这个就会导致URL的equals的方法违背了equals约定。URL的equals方法是一个巨大的错误且不应该被模仿,但是由于兼容性的问题,它不能被改变。为了防止这类问题,equals方法应该只对内存驻留对象进行确定性的计算。

Non-nullity

对于任何非空引用x,x.equals(null)必须返回false
equals方法的最后一个约定并没有官方的名称,所以effective Java作者给其起名为“Non-nullity”,也是说所有的对象都和null不相等。

建议方式

下面是重写高质量equals的建议步骤

  1. 使用 == 操作符来检查参数是否引用当前对象
  2. 使用instanceof操作符来检查参数是否为正确的类型
  3. 将参数强制转换为正确的类型
  4. 对于类中每个重要的字段,都检查参数的字段是否与当前对象的字段相等。
    如果以上条件都满足的话,那么equals方法返回true,否则返回false。
    对于以上的第2个步骤,如果是接口,可以通过接口的方法来获取参数对象的字段,如果是类,可能直接访问,如果访问修饰符允许的话。
    比较的字段的方式:
    a. 原生数据类型float,使用Float.compare(float, float)来判断是否相等
    b. 原生数据类型double,使用Double.compare(double, double)来判断是否相等
    c. 其他原生数据类型,使用 ==来判断是否相等
    d. 引用类型,直接使用equals方法来判断是否相等
    f. 数组,可以针对每个数组元素使用上述方式进行比较,也可以通过Arrays.equals方法进行比较

注意:在比较两个引用类型对象时,可以使用Objects.equals(Object, Object)从而避免可能出现的NPE异常。equals方法的性能可能因为字段比较的顺序而产生影响,因此,应该首先比较最可能出现不同值的字段,这种代价最低。也可以比较衍生字段,比如四边形的面积,当面积都不相等时,就无需再分别比较长和宽了。
最后,还有一些建议:

  1. 重写equals方法时总是重写hashcode
  2. 不要用另一种类型替换 equals 声明中的对象
    public boolean equals(MyClass o) {}
    这样写相当于重载了Object的equals方法,它会导致子类中的重写注释产生误报并提供错误的安全性。一致使用Override注解会阻止你犯此错误。因为这样equals方法将不会编译通过,而且会提示相应的错误消息。
    @Override public boolean equals(MyClass o) {}
  3. 重写equals方法最好是通过Google的AutoValue框架或者IDE来自动生成,而不是手动重写。

总结,若非必要就不要重写equals方法,直接从Object继承即可,若重写了equals,请确保对类的所有重要字段都进行了比较,且不能打破equals方法的约定。

相关文章

网友评论

      本文标题:重写equals方法时遵守通用约定

      本文链接:https://www.haomeiwen.com/subject/fgguyqtx.html