0 正确的equals方法

  1. public class MyClass {
  2. // 主要属性1
  3. private int primaryAttr1;
  4. // 主要属性2
  5. private int primaryAttr2;
  6. // 可选属性
  7. private int optionalAttr;
  8. // 延迟加载,缓存散列码
  9. private volatile int hashCode = 0;
  10. @Override
  11. public int hashCode() {
  12. if(hashCode == 0) {
  13. int result = 17;
  14. result = 37*result + primaryAttr1;
  15. result = 37*result + primaryAttr2;
  16. hashCode = result;
  17. }
  18. return hashCode;
  19. }
  20. @Override
  21. public boolean equals(Object obj) {
  22. if(obj == this) {
  23. return true;
  24. }
  25. if(!(obj instanceof MyClass)) {
  26. return false;
  27. }
  28. MyClass myClass = (MyClass) obj;
  29. return myClass.primaryAttr1 == primaryAttr1 &&
  30. myClass.primaryAttr2 == primaryAttr2;
  31. }
  32. }

1 在改写equals时要遵守通用约定

1.1 不必改写equals的情况:

1)一个类的每个实例本质上都是惟一的。
2)不关心一个类是否提供了“逻辑相等”的测试功能。
3)超类已经改写的equals,从超类继承的行为对于子类也是合适的。
4)一个类是私有的,或者是包级私有的,并且可以确定它的equals方法永远也不会被调用。

1.2 需要改写Object.equals的情况:

当一个类有自己特有的“逻辑相等”概念,而且父类也没有改写equals以实现期望的行为。通常适合于value class的情形。
改写equals也使得这个类的实例可以被用作map的key或者set的元素,并使map和set表现出预期的行为。

1.3 改写equals要遵守的通用约定(equals方法实现了等价关系):

1)自反性:x.equals(x)一定返回true
2)对称性:x.equals(y)返回true当且仅当y.equals(x)
3)传递性:x.equals(y)且y.equals(z),则x.equals(z)为true
4)一致性:若x.equals(y)返回true,则不改变x,y时多次调用x.equals(y)都返回true
5)对于任意的非空引用值x,x.equals(null)一定返回false。

当重写完equals方法后,应该检查是否满足对称性、传递性、一致性。(自反性、null通常会自行满足)

下面是一个示例:

  1. public class Point {
  2. private final int x;
  3. private final int y;
  4. public Point(int x, int y) {
  5. this.x = x;
  6. this.y = y;
  7. }
  8. public boolean equals(Object o) {
  9. if(!(o instanceof Point))
  10. return false;
  11. Point p = (Point)o;
  12. return p.x == x && p.y == y;
  13. }
  14. //...
  15. }
  1. import java.awt.Color;
  2. public class ColorPoint extends Point {
  3. private final Color color;
  4. public ColorPoint(int x, int y, Color color) {
  5. super(x, y);
  6. this.color = color;
  7. }
  8. /* 不满足对称性 */
  9. public boolean equals(Object o) {
  10. if(!(o instanceof ColorPoint))
  11. return false;
  12. ColorPoint cp = (ColorPoint)o;
  13. return super.equals(o) && cp.color == color;
  14. }
  15. //...
  16. }
  1. import java.awt.Color;
  2. public class Test {
  3. public static void main(String arg[])
  4. {
  5. System.out.println("检验对称性:");
  6. Point p = new Point(1, 2);
  7. ColorPoint cp = new ColorPoint(1, 2, Color.RED);
  8. System.out.println(p.equals(cp));
  9. System.out.println(cp.equals(p));
  10. System.out.println("检验传递性:");
  11. ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
  12. Point p2 = new Point(1, 2);
  13. ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
  14. System.out.println(p1.equals(p2));
  15. System.out.println(p2.equals(p3));
  16. System.out.println(p1.equals(p3));
  17. }
  18. }

此时的输出为:

  1. 检验对称性:
  2. true
  3. false
  4. 检验传递性:
  5. false
  6. true
  7. false

这表示上述equals方法的写法违背了对称性,但没有违背传递性。之所以不满足对称性,是由于Point的equals方法在接收ColorPoint类型的参数时,会将其当做Point(忽略color属性)进行比较,而ColorPoint的equals方法接收Point类型参数时,判断其不是ColorPoint类型就直接返回false。因此,可以对equals方法作如下修改:

  1. /* 满足对称性,但牺牲了传递性  */
  2. public boolean equals(Object o) {
  3. if(!(o instanceof Point))// 不是Point类型
  4. return false;
  5. if(!(o instanceof ColorPoint))// 是Point类型,但不是ColorPoint类型
  6. return o.equals(this);
  7. // 是ColorPoint类型
  8. ColorPoint cp = (ColorPoint)o;
  9. return super.equals(o) && cp.color == color;
  10. }

此时的执行输出为:

  1. 检验对称性:
  2. true
  3. true
  4. 检验传递性:
  5. true
  6. true
  7. false

这表示满足对称性,但牺牲了传递性。由于p1与p2,p2与p3的比较都没有考虑color属性,但是p1与p3比较则考虑了color属性。要在扩展(即继承)一个可实例化的类的同时,既要增加新的属性,同时还要保留equals约定,没有一个简单的方法可以做到。

复合优先于继承。遵照这条原则,这个问题可以有很好的解决方案,不再让ColorPoint继承Point,而是在ColorPoint类中加入一个私有的Point域,以及一个公有的view方法,此方法返回对应的Point对象。

  1. import java.awt.Color;
  2. public class ColorPoint {
  3. private Point point;
  4. private final Color color;
  5. public ColorPoint(int x, int y, Color color) {
  6. point = new Point(x, y);
  7. this.color = color;
  8. }
  9. // 返回ColorPoint的Point视图(view)
  10. public Point asPoint() {
  11. return point;
  12. }
  13. public boolean equals(Object o) {
  14. if(!(o instanceof ColorPoint))
  15. return false;
  16. ColorPoint cp = (ColorPoint)o;
  17. return cp.point.equals(point) && cp.color.equals(color);
  18. }
  19. // ...
  20. }

Java平台中有一些类是可实例化类(非抽象类)的子类,且加入了新属性。例如,java.sql.Timestamp继承自java.util.Date,并增加了nanoseconds域,Timestamp的equals违反了对称性,当Timestamp和Date用于集合中时可能出现问题。

注意,可以在一个抽象类的子类中增加新的属性,而不会违反equals约定。因为不可能创建父类的实例,也就不会出现上述问题。

2 不要将equals声明中的Object对象替换为其他类型

  1. public boolean equals(MyClass obj) {
  2. ...
  3. }

上述代码,使用了具体的类MyClass作为参数,这会导致错误。原因在于,这个方法并没有重写(override)Object.equals方法,而是重载(overload)了它。某些情况下,这个具体化的equals方法会提高一些性能,但这样极有可能造成不易察觉的错误。

3 改写equals时同时改写hashCode

3.1 java.lang.Object的规范中,hashCode约定:

1)在一个应用程序执行期间,如果一个对象的equals方法
2)相等对象(equals返回true)必须具有相等的散列码
3)不相等对象(equals返回false)调用hashCode方法,不要求必须产生不同的值;但是产生不同的值有可能提高散列表的性能。

若改写equals时,未同时改写hashCode方法,则可能导致不满足第二条。

3.2 改写hashCode的常用方法:

1)把某个非零常数值,如17,保存在一个叫result的int类型变量中;
2)对与对象中每个关键域f(equals方法中考虑的每一个域),完成以下步骤:
2.1)为该域计算int类型的散列码c:
2.1.1)若该域为boolean类型,计算(f?0:1);
2.1.2)若该域为byte、char、short、int类型,计算(int)f;
2.1.3)若该域为long,计算(int)(f ^ (f>>>32));
2.1.4)若该域为float,计算Float.floatToIntBits(f);
2.1.5)若该域为double,先计算Double.doubleToLongBits(f)得到一个long值,再按2.1.3对long值计算散列值;
2.1.6)若该域为一个对象引用,且该类的equals方法通过递归调用equals的方式比较这个域,则同样对这个域递归调用hashCode方法。
2.1.7)若该域为一个数组,则把每一个元素当做单独的域来处理。即对每个元素递归地应用上面6条,然后根据2.2的做法把这些散列值组合起来。
2.2)按照下面的公式,把步骤2.1中得到的散列码c组合到result中:
result = 37*result + c;
3)返回result。
4)考查重写后的hashCode方法是否满足“相等的实例具有相等的散列码”

参考资料:

《Effective Java》(2nd Edition) 第7条、第8条、第14条、第20条、第37条等

该书上貌似目前就推荐使用复合而不是继承,所以最终解决重写equals的传递性的方案是复合。如果各路大神有好的方式麻烦告诉我,在此谢谢了

重写equals时,遵守的规定的更多相关文章

  1. 为什么重写equals时必须重写hashCode方法?(转发+整理)

    为什么重写equals时必须重写hashCode方法? 原文地址:http://www.cnblogs.com/shenliang123/archive/2012/04/16/2452206.html ...

  2. 讲解:为什么重写equals时必须重写hashCode方法

    一 :string类型的==和equals的区别: 结论:"=="是判断两个字符串的内存地址是否相等,equals是比较两个字符串的值是否相等,具体就不做扩展了,有兴趣的同学可以去 ...

  3. 第10项:重写equals时请遵守通用约定

      重写equals方法看起来似乎很简单,但是有许多重写方式会导致错误,而且后果非常严重.最容易避免这类问题的办法就是不覆盖equals方法,在这种情况下,类的每个实例都只能与它自身相等.如果满足了以 ...

  4. 为什么重写equals时必须重写hashCode方法?

    原文地址:http://www.cnblogs.com/shenliang123/archive/2012/04/16/2452206.html 首先我们先来看下String类的源码:可以发现Stri ...

  5. 第8条:覆盖equals时遵守通用约定

    如果不需要覆盖equals方法,那么就无需担心覆盖equals方法导致的错误. 什么时候不需要覆盖equals方法? 1.类的每个实例本质上是唯一的. 例如对于Thread,Object提供的equa ...

  6. java中为什么重写equals时必须重写hashCode方法?

    在上一篇博文Java中equals和==的区别中介绍了Object类的equals方法,并且也介绍了我们可在重写equals方法,本章我们来说一下为什么重写equals方法的时候也要重写hashCod ...

  7. why在重写equals时还必须重写hashcode方法

    首先我们先来看下String类的源码:可以发现String是重写了Object类的equals方法的,并且也重写了hashcode方法 public boolean equals(Object anO ...

  8. 为什么重写equals时一定要重写hashcode

    我们开发时写一个类,默认继承Object类,Object类的equals方法是比较是否指向同一个对象(地址是否相同), Object类 的hashcode方法返回的对象内存地址的值, 一个类只重写了e ...

  9. Effective Java —— 覆盖equals时遵守通用约定

    本文参考 本篇文章参考自<Effective Java>第三版第十条"Obey the general contract when overriding equals" ...

随机推荐

  1. 【一天一道LeetCode】#20. Valid Parentheses

    一天一道LeetCode系列 (一)题目 Given a string containing just the characters '(', ')', '{', '}', '[' and ']', ...

  2. JNI动态注册native方法及JNI数据使用

    前言 或许你知道了jni的简单调用,其实不算什么百度谷歌一大把,虽然这些jni绝大多数情况下都不会让我们安卓工程师来弄,毕竟还是有点难,但是我们还是得打破砂锅知道为什么这样干吧,至少也让我们知道调用流 ...

  3. OpenCV 使用光流法检测物体运动

    OpenCV 可以使用光流法检测物体运动,贴上代码以及效果. // opticalflow.cpp : 定义控制台应用程序的入口点. // #include "stdafx.h" ...

  4. Android下用Activity实现圆角的自定义弹窗

    这里我们使用9patch制作一个背景,找到SDK目录下的tools目录,双击draw9patch.bat文件,如下图: 打开想要编辑的文件,进行编辑(9patch的编辑教程自寻). 编辑完成后,保存到 ...

  5. Cocos2D的OALSimpleAudio预加载音频

    在OALSimpleAudio接口中,没有委托机制和通知机制告诉你什么时候预加载完成.也不需要这样的机制. 如下代码: //Listing 11-1. Preloading a background ...

  6. ExtJS:菜单ComboBox及级联菜单应用

    首页一级菜单查询分组,二级菜单查询分组中的车辆信息. 定义分组数据模型: Ext.define( 'group', { extend:'Ext.data.Model', fields:[ {name: ...

  7. Leetcode_198_House Robber

    本文是在学习中的总结,欢迎转载但请注明出处:http://blog.csdn.net/pistolove/article/details/47680663 You are a professional ...

  8. Android 4.1.2系统添加重启功能

    对于Android的的手机或者平板长期使用,感觉会出现慢的情况,所以偶尔还是需要重启一下,而长按电源键弹出的菜单又没有重启选项,所以特在此记录自己添加这个功能的过程. 首先关机的那个弹出菜单是在fra ...

  9. linux的link命令

    sudo ln -s 源文件 目标文件 sudo ln -s /usr/local/mysql/bin/mysqladmin /sbin/mysqladmin 建立软连接 ln -d existfil ...

  10. 【Android 应用开发】BluetoothSocket详解

    一. BluetoothSocket简介 1. 简介 客户端与服务端 : BluetoothSocket 和 BluetoothServerSocket 类似于Java中的套接字的 Socket 和 ...