本文结合《Effective Java》第三章条目8《覆盖equals时请遵守通用约定》和自己的理解及实践,讲解了在覆盖equals时需要遵守的规范,文章发布于专栏Effective Java,欢迎读者订阅。
Java中用equals方法来判断两个对象是不是相等,equals方法是Object类就拥有的方法,因而属于所有对象通用的方法,使用方式很简单:a.equals(b) ,返回true或false。下面进入正题。
我们都知道,如果不覆盖equals方法,那么就是使用的父类的equals方法,我们可以来看看Object的equals方法都做了什么:
public boolean equals(Object obj) { return (this == obj); }
总结一下:如果类具有自己特有的“逻辑相等”概念,而且父类还没有覆盖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
> 一致性
对于不等于null的x和y,只要对象中,被equals操作使用的信息没有被修改,那么多次调用x.equals(y),要么一直返回true,要么一直返回false。
要想严格遵循这一条约定,必须保证equals方法里面不依赖外部不可靠的资源,如果equals方法里面依赖了外部的资源,就很难保证具有一致性了。
> 非空性
对于不等于null的x,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; } }
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 }
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)); }
看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; }
所以,如果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方法?
很多人喜欢这样做:
public boolean equals(MyClass o) ...
问题在于,这个方法根本没有覆盖equals方法。
因为有逻辑相等的判断需要,所以在父类没有覆盖的情况下,我们才需要覆盖equals方法。
编写equals方法需要遵循五条军规:自反、对称、传递、一致、非空。
很多Java提供的接口和类都调用了equals方法,不符合军规的equals方法,会造成很多函数的结果不可预知。