Java equals方法编写规范 —— 牢记这5条军规

本文结合《Effective Java》第三章条目8《覆盖equals时请遵守通用约定》和自己的理解及实践,讲解了在覆盖equals时需要遵守的规范,文章发布于专栏Effective Java,欢迎读者订阅。


Java中用equals方法来判断两个对象是不是相等,equals方法是Object类就拥有的方法,因而属于所有对象通用的方法,使用方式很简单:a.equals(b)  ,返回true或false。下面进入正题。


什么时候才应该覆盖equals方法

我们都知道,如果不覆盖equals方法,那么就是使用的父类的equals方法,我们可以来看看Object的equals方法都做了什么:

    public boolean equals(Object obj) {
        return (this == obj);
    }

显然,Object只是使用==运算符,简单地判断两个对象是不是同一个对象,也就是说,new出来的两个对象,不管他们属性是不是相同,都是不相等的。而实际使用中,我们常常会碰到“逻辑相等”的需求,比如,我们认为两个半径相同的圆,他们是相等的,这个时候,如果圆的父类,还没有覆盖equals方法实现这个逻辑相等,那么,就需要在类里面去覆盖equals方法。

总结一下:如果类具有自己特有的“逻辑相等”概念,而且父类还没有覆盖equals方法实现期望的逻辑,这时候就需要我们覆盖equals方法。


equals方法的五条约定

由于在很多集合的内部方法中,都会使用到equals方法,比如contains方法,因此我们在覆盖equals方法的时候,需要遵循以下规定,否则会造成异常。

对于不等于null的x、y、z,有以下规定:

> 自反性

x.equals(x)==true。

> 对称性

y.equals(x)==true  <---> x.equals(y)==true。

通过下一节的例子你可以加深这条约定的理解。

> 传递性

x.equals(y)==true,y.equals(z)==true  ---> x.equals(z)==true

> 一致性

对于不等于nullx和y,只要对象中,被equals操作使用的信息没有被修改,那么多次调用x.equals(y),要么一直返回true,要么一直返回false。

要想严格遵循这一条约定,必须保证equals方法里面不依赖外部不可靠的资源,如果equals方法里面依赖了外部的资源,就很难保证具有一致性了。

> 非空性

对于不等于nullx,x.equals(null)必须返回false。要遵守这个约定,只需要在equals里面加上这么一段代码 if(o == null) return false,然而,这样做往往是没有必要的,因为我们都会在equals方法的第一步,做instanceof校验,检查参数是否为正确的类型。而a.instanceOf(null)会返回false。


一个简单的例子  告诉你为什么要遵守约定

假设有一个类,持有一个String对象,并且在比较时不区分大小写,代码如下,重点关注一下它的equals方法实现:

public final class CaseInsensitiveString {
    private final String s;

    public CaseInsensitiveString(String s) {
        if (s == null)
            throw new NullPointerException();
        this.s = 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;
    }
}

equals方法这样写的出发点是非常好的,它试图提供和普通String类型进行比较的能力,然而,它却违反了对称性的约定,因为String不知道有这个类,运行下面这段代码:

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

cis.equals(s)==true,但是s.equals(cis)==false,违反了对称性的规定,而这会造成什么危害呢? 看看下面这段代码,你觉得会打印出true还是false?

    public static void main(String[] args) {
        CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
        String s = "polish";

        List<CaseInsensitiveString> list = new ArrayList<>();
        list.add(cis);
        System.out.println(list.contains(s));
    }

由于equals方法不符合对称性的约定,因此打印true还是false,取决于ArrayList方法对contains方法的实现,如果他内部实现是cis.equals(s),那么会返回true,如果是s.equals(cis),那么会返回false。

看ArrayList的实现:

    public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }

    public int indexOf(Object o) {
        if (o == null) {
            for (int i = 0; i < size; i++)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = 0; i < size; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

可以看出,根据ArrayList的内部实现,contains方法最终会执行这样的代码:s.equals(cis),所以他会返回false。

所以,如果equals方法违反了约定,很多行为的结果将不可预知。


实现equals方法的诀窍

结合这五个约定,我们总结一下实现高质量equals方法的诀窍:

1. 使用==检查参数是否为这个对象的引用,是,则直接返回true,提供判断的效率。

2. 使用instanceof检查参数是否为正确的类型,如果不是,返回false。

3. 把参数转成正确的类型。因为在第二步已经做了instanceof校验,所以能够确保这一步不会出错。

4. 对于类中每一个关键的属性,也就是“逻辑相等”需要判断的属性,逐一比较参数的这些属性是否和对象的一致。比较时需要注意一下细节:

1) float和double类型的属性,要使用Float.compare(f1,f2)或者Double.compare(d1,d2)方法进行比较

2) 对象类型的属性,使用equals方法比较,有些属性可能为null,为了避免出现空指针异常,可以采用这样的方式:

(field ==null ? o.field == null : field.equlas(o.field))

3) 对于数组类型的属性,则要把这些原则用到每个元素上,如果每个元素都很重要,可以考虑使用Arrays.equals方法

4) 比较顺序会影响equals方法的性能,为了获得最佳的性能,应该最先比较最有可能不一致的属性或者开销最低的属性。

5.  当你编写完equals方法后,请检查是否符合五条军规——自反、对称、传递、一致、非空,写单元测试校验!


其他注意事项

> 覆盖了equals方法之后一定要覆盖hashcode方法

在专栏的另一篇文章里,我做了解释,为什么覆盖了equals方法之后一定要覆盖hashCode方法?

> 不要把equals方法的入参类型改为非Object的

很多人喜欢这样做:

public boolean equals(MyClass o) ...

问题在于,这个方法根本没有覆盖equals方法。


总结

因为有逻辑相等的判断需要,所以在父类没有覆盖的情况下,我们才需要覆盖equals方法。

编写equals方法需要遵循五条军规:自反、对称、传递、一致、非空

很多Java提供的接口和类都调用了equals方法,不符合军规的equals方法,会造成很多函数的结果不可预知。



你可能感兴趣的:(覆盖,equals,约定)