Java equals

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
一致性 对于不等于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;  
}   }

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?
[java] view plain copy
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的实现:
[java] view plain copy
public boolean contains(Object o) {
return indexOf(o) >= 0;
}

[java] view plain copy
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方法,会造成很多函数的结果不可预知。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值