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. Android群英传笔记——第九章:Android系统信息和安全机制

    Android群英传笔记--第九章:Android系统信息和安全机制 本书也正式的进入尾声了,在android的世界了,不同的软件,硬件信息就像一个国家的经济水平,军事水平,不同的配置参数,代表着一个 ...

  2. C++中将构造函数或析构函数定义为private

    今天面试被问到了这个单例模式常用到的技术手段,下面进行分析: 很多情况下要求当前的程序中只有一个object.例如一个程序只有一个和数据库的连接,只有一个鼠标的object.通常我们都将构造函数的声明 ...

  3. STL常用查找算法介绍

    adjacent_find() 在iterator对标识元素范围内,查找一对相邻重复元素,找到则返回指向这对元素的第一个元素的迭代器.否则返回past-the-end. #include <io ...

  4. 浅析数据结构中栈与C实现

    最近在搞摄像头驱动,o()︿︶)o 唉,别提有多烦,一堆寄存器就有人受的了--特么这不是单片机的开发,这是内核驱动开发-- 今天放松一下,我们来看看数据结构中的栈,这节的知识点可以说是数据结构中最容易 ...

  5. linux设置系统时间

    设置系统时间 -         date命令:显示系统的时间,可以在直接输入"date"命令来查看系统的时间 -           date+%y/%m/%d -        ...

  6. “《编程珠玑》(第2版)第2章”:A题(二分搜索)

    A题是这样子的: 给定一个最多包含40亿个随机排列的32位整数的顺序文件,找出一个不在文件中的32位整数(在文件中至少缺失一个这样的数据——为什么?).在具有足够内存的情况下,如何解决该问题?如果有几 ...

  7. Mybatis批量插入、批量更新

    合理的使用批量插入.更新对优化有很大的作用,速度明显快了N倍. 数据库连接串后面要新增:&allowMultiQueries=true 批量插入的最大限制主要是看你整条sql占用的大小,所以可 ...

  8. DB Query Analyzer 5.03 is distributed, EXCEL table name will be enclosed in square bracket

      DB Query Analyzer 5.03 is distributed, table name will be enclosed in square bracket automatically ...

  9. Android流媒体开发之路二:NDK开发Android端RTMP直播推流程序

    NDK开发Android端RTMP直播推流程序 经过一番折腾,成功把RTMP直播推流代码,通过NDK交叉编译的方式,移植到了Android下,从而实现了Android端采集摄像头和麦克缝数据,然后进行 ...

  10. MyBatis 框架之快速入门程序

    一.使用 IDEA 快速创建 Maven 项目 关于如何快速创建 Maven 项目,这个可以参考下面这篇文章: Maven 项目管理工具基础入门系列(一) 二.快速配置 MyBatis 依赖 jar ...