Java equals方法编写规范 —— 牢记这5条军规
来源:互联网 发布:ios10不安全网络怎么连 编辑:程序博客网 时间:2024/06/06 20:55
本文结合《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
> 一致性
对于不等于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?
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方法,会造成很多函数的结果不可预知。
- Java equals方法编写规范 —— 牢记这5条军规
- 第二条军规——程序元素命名要规范
- 牢记!SQL Server数据库开发的二十一条军规
- 股民必须牢记的二十二条军规
- 牢记!SQL Server数据库开发的二十一条军规
- 股民必须牢记的“二十一条军规”
- 牢记!SQLServer开发的二十一条军规
- 牢记!SQL Server数据库开发的二十一条军规
- 牢记!SQL Server数据库开发的二十一条军规
- 牢记!SQL Server数据库开发的二十一条军规
- 牢记!SQL Server数据库开发的二十一条军规
- 牢记!SQL Server数据库开发的二十一条军规
- 牢记!SQL Server数据库开发的二十一条军规
- 牢记!SQL Server数据库开发的二十一条军规
- 牢记!SQL Server数据库开发的二十一条军规
- 牢记!SQL Server数据库开发的二十一条军规
- 牢记!SQL Server数据库开发的二十一条军规
- 牢记!SQL Server数据库开发的二十一条军规
- python学习——IO编程——文件读写
- 为什么要重写hashcode()
- EmberJS快速入门(二)
- Android6.0系统悬浮窗权限的问题解决方法
- qt sqlite数据库操作
- Java equals方法编写规范 —— 牢记这5条军规
- Information:Gradle tasks [:app:clean, :app:generateDebugSources和transformClassesWithJarMergingForDeb
- build cef 3.3029.1619.geeeb5d7
- JSP页面中插入css样式的三种方法
- JavaScript中的递归、PTC、TCO和STC
- STM32F4-Discovery
- Python基础-字符串格式化_百分号方式_format方式(转)
- 每天学一点Swift----面向对象上(八)
- attr()