java吧 关注:1,263,394贴子:12,765,008

深入equals方法重写带了的问题 - 读effectiveJava做的笔记

只看楼主收藏回复

之前在某个帖子中讨论到了equals方法的重写问题,但表述的不清楚,感觉有必要把这个问题好好表述一下,这些笔记是当时读effectiveJava时做的,认为作者分析的非常精彩,很是佩服。今晚又花了很长时间{ps:现在时间相当晚了已经},重新整理了一下要点,分享给大家,重点在后边,请大家耐心看。
基本概念:重写的equals一般都是用来判断逻辑上的相等, == 则是完全相等。
effective -java 上说,重写 equals方法看起来很简单,但是在很多情况下很容易导致问题,这些问题有时会导致严重后果 { 个人认为后果可能是问题很隐蔽,不容易找出来 } 。最简单的办法就是尽量避免重写 equals 方法。
有些时候 equals 方法是不需要的 :
1、程序中没必要关心他提供的equals是否能够返回真正的逻辑相等,比如java.util.Random
2、单例或每个实例本身满足唯一性,比如说线程Thread{貌似是这个意思:Each instance of the class is inherently unique}
3、父类已经重写了equals,子类最好不要重写{后面会描述重写子类equals带来的纠结问题}
什么时候应该重写equals方法:
1、期望通过equals方法来判断两个对象是否是逻辑上相等。
2、想将对象用做map的key值或set的元素时,同时要重写hashCode方法。
接下来描述一下重写equals应当要满足的条件:
1. 自等:x.equals(x) == true 这个是最起码的要求
2. 对称:x.equals(y) == true ,必须有 y.equals(x) == true;
3. 传递:x.equals(y) == true,x.equals(z) == true ;必须有 y.equals(z) == true
4. 非空:x.equals(null) 应总是返回false;
5. 恒定(consistent),x.equals(y) 应恒返回true或false, 以支持用equals来判断对象是否被修改



IP属地:北京1楼2012-06-03 00:30回复
    接下来讨论一下 父类重写了equals方法之后、子类也重写了equals容易导致的问题,引用effective-java的例子
    /** parent Code */
    public class Point {
    private int x;
    private int y;
    public Point (int x, int y){
    this.x = x;
    this.y = y;
    }
    public boolean equals(Object o){
    if(null == o) return false;
    if(this == o) return true;
    if(!(o instanceof Point)){
    return false;
    }
    Point p = (Point)o;
    return p.x == x && p.y == y;
    }
    }
    接下来继承这个类
    public class ColorPoint extends Point{
    private Color color;
    public ColorPoint(int x, int y, Color color) {
    super(x, y);
    this.color = color;
    }
    }
    这个子类的equals怎么写呢,先看第一种写法。{为了简单,不再加判断为空、自等的代码了;这里重写equals时是要把color加入到逻辑判断中,如果不加入的话,直接继承父类的就可以了,也就没有下边的问题了}
    a) public boolean equals(Object o) {
    if(!(o instanceof ColorPoint)){
    return false;
    }
    ColorPoint cp = (ColorPoint) o;
    return super.equals(o) && cp.color == color;
    }
    为了验证这个对不对,来声明两个对象
    Point p = new Point(1, 2);
    ColorPoint cp = new ColorPoint(1, 2, Color.RED);
    结果是: p.equals(cp) returns true, while cp.equals(p) returns false;原因是颜色的比较导致的。
    试图修复这个问题,首先想到是假如传入的对象不是ColorPoint,就不会比较color。
    b) public boolean equals(Object o) {
    if (!(o instanceof Point)){
    //o 连Point都不是
    return false;
    }
    if(!(o instanceof ColorPoint)){
    //o 就是单纯的一个Point
    return super.equals(o);
    }
    //o 是一个ColorPoint
    ColorPoint cp = (ColorPoint) o;
    return super.equals(o) && cp.color == color;
    }
    这个能够解决上边的bug,支持了对称性,但是不支持传递性.
    ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
    Point p2 = new Point(1, 2);
    ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
    很明显能够发现,p1.equals(p2), p2.equals(p3)返回的是true; 但是p1.equals(p3)返回false。原因是前两个比较是”色盲“。
    


    IP属地:北京2楼2012-06-03 00:36
    回复
      2025-06-09 09:57:15
      广告
      public class ColorPoint {
      private Point point;
      private String color;
      public ColorPoint(int x, int y, String color) {
      point = new Point(x, y);
      this.color = color;
      }
      /** * @return 返回 Point 视图 */
      public Point asPoint(){
      return point;
      }
      //equals 由于没有继承,就是最基本的equals写法。
      }
      代码的意思就是,ColorPoint不再是Point了,通过Point的equals方法,返回的是false;如果要拿ColorPoint和Point去比较坐标,ColorPoint也提供了Point视图来支持这个功能。
      值得注意的是,在jdk的api中,有一些特殊的类是采用的继承父类,在添加属性之后又重写了父类的equals,比如 java.sql.Timestamp extends java.util.Date;这里Timestamp在重写的equals添加了 nanoseconds,就没有能够满足对称条件,API中加了个免责声明:Note: This method is not symmetric with respect to the equals(Object) method in the base class.也就是说这个Timestamp和Date 尽量避免混淆,而且只能由程序员自己来控制。这个在个人的代码中是不推荐模仿的。
      最后一点,如果重写了equals方法,一定要注意重写hashCode方法,以支持对象能够正确的返回Hash值,否则在当对象放到HashMap或者HashSet中时,会产生很隐蔽的bug。
      


      IP属地:北京3楼2012-06-03 00:38
      回复
        最上边丢了句话,补上:
        通过研究发现,这个是面向对象语言中基本等价关系的问题,没有办法在扩展了一个类之后还能保持这种等价关系。不过在遵从”组合优于继承“的原则,可以找到一个办法来绕过这个问题,让 ColorPoint不再继承自 Point,而是包含 Point, 意思是 ColorPoint 有一个Point,但他不是Point。
        


        IP属地:北京4楼2012-06-03 00:40
        回复

          其实我感觉只要会this用法,知道调用的内存分析,equals也不难


          IP属地:北京5楼2012-06-03 00:51
          回复


            IP属地:北京6楼2012-06-03 00:52
            回复
              "结果是: p.equals(cp) returns true, while cp.equals(p) returns false;原因是颜色的比较导致的。"
              这一句是最大的说法错误.原来根本不是颜色的比较导致的,就算你将:
              "return super.equals(o) && cp.color == color;"
              改为:
              "return super.equals(o) && cp.color.equals(this.color);"
              一样会返回false,这根本和颜色比较没有关系.使cp.equals(p)返回false的原因只有一个,那就是:
              cp.equals(p)方法中的参数p是Point对象,该方法被调用后,ColorPoint类中的equals()方法被执行,请看以下被执行到的语句:
              if(!(o instanceof ColorPoint)){
              return false;
              }
              很明显,参数p是Point对象,而Point是父类,ColorPoint是子类,可以说子类是父类,但是不可以说父类是子类,所以参数p不是ColorPoint对象,那么就会执行到if语句块的代码,所以cp.equals(p)被调用后自然会返回false,明白吗,还根本就没执行到"判断颜色是否相等"的那一步上就已经返回false了.
              "这个能够解决上边的bug,支持了对称性,但是不支持传递性.
              ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
              Point p2 = new Point(1, 2);
              ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
              很明显能够发现,p1.equals(p2), p2.equals(p3)返回的是true; 但是p1.equals(p3)返回false。原因是前两个比较是”色盲“。"
              这种说法又是错误的,这确定是该书上写的代码吗,我严重怀疑你是不是看错了原教材中的什么地方的代码?好吧,我们一步一步来,请先看你修改后的代码:
              public boolean equals(Object o) {
              if (!(o instanceof Point)){
              //o 连Point都不是
              return false;
              }
              if(!(o instanceof ColorPoint)){
              //o 就是单纯的一个Point
              return super.equals(o);
              }
              //o 是一个ColorPoint
              ColorPoint cp = (ColorPoint) o;
              return super.equals(o) && cp.color == color;
              }
              你没有搞错吧,return super.equals(o) && cp.color == color;这句代码中居然是判断两个对象的颜色的内存地址是否一致,这明显不会一致啊:
              ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
              Point p2 = new Point(1, 2);
              ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
              p1和p3的构造方法里传的明显是两个不同内存地址的Color对象,就算没有ColorPoint不继承Point类,你只要对两个不同ColorPoint对象中的Color属性传递不同内存地址的Color对象,你这样比较自然是永远返回false了,因为Color是个引用类型啊,大哥,要比较引用类型是否相等,不是比较两个对象的内存地址的好不好.假如你把return语句改为:
              return super.equals(cp) && cp.color.equals(color);
              你再试试看:
              System.out.println(p1.equals(p2));
              System.out.println(p2.equals(p3));
              System.out.println(p1.equals(p3));
              三个打印语句是不是都返回true了.
              我就不明白了,你判断两个color对象是否相等,你居然是判断两个color对象的内存地址是否相等,不知道你是咋想的.我也不相信这真是书上写的,我相信书中写不出这样的return语句来.也想不通这怎么跟"对称性"和"传递性"扯上关系了.


              IP属地:辽宁7楼2012-06-03 01:42
              回复

                Color.RED 和 Color.BULE 是常量,可以用 == 来判断


                IP属地:北京8楼2012-06-03 08:58
                回复
                  2025-06-09 09:51:15
                  广告

                  "结果是: p.equals(cp) returns true, while cp.equals(p) returns false;原因是颜色的比较导致的。"
                  这个描述是我添的,不是树上的,表示确实错了,本来应该描述第二段代码的,放错位置了。。谢谢 ,


                  IP属地:北京9楼2012-06-03 09:02
                  回复
                    应该是你说的那样:while the latter comparison always returns false because the type of the argument is incorrect


                    IP属地:北京10楼2012-06-03 09:07
                    回复
                      你别说笑了,虽然Color是个单例设计模式的类,但是Color.RED和Color.BULE返回的却是两个不同的Color对象.你用==来判断两个不同的对象,你觉得有%1的可能会返回true么.


                      IP属地:辽宁11楼2012-06-03 15:18
                      回复
                        知错能改,就是好样的


                        IP属地:辽宁12楼2012-06-03 15:19
                        回复
                          嗯,你想通了就好.


                          IP属地:辽宁13楼2012-06-03 15:23
                          回复
                            终于明白你要表达啥了,我要比较 Color.RED 和 Color.BULE,就是为了说明“两个 ColorPoint不同,但是他们却都能够和Point相等,这是不合理的”,假如两个对象都是 Color.RED,那比较起来就没有意义了。


                            IP属地:北京14楼2012-06-04 11:30
                            回复
                              2025-06-09 09:45:15
                              广告
                              你用Color来比较本来就是错误的做法."Color.某种颜色的常量字段"将返回某种颜色的唯一对象(单例设计模式),也就是说Color.RED和Color.BULE将生成两个不同的Color对象,你用==来判断这两个对象是否相等?这不摆明了永远都不可能相等吗,你看看你写的equals()方法的代码:
                              public boolean equals(Object o) {
                              if (!(o instanceof Point)){
                              //o 连Point都不是
                              return false;
                              }
                              if(!(o instanceof ColorPoint)){
                              //o 就是单纯的一个Point
                              return super.equals(o);
                              }
                              //o 是一个ColorPoint
                              ColorPoint cp = (ColorPoint) o;
                              return super.equals(o) && cp.color == color;
                              }
                              你自己读一读:
                              return super.equals(o) && cp.color == color;
                              如果传入两个不同的Color对象,你认为两个ColorPoint类的equals()方法有那么一丁点可能会返回true吗?你还把"对称性"和"传递性"也扯进来了,是不是搞笑了点儿.
                              ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
                              Point p2 = new Point(1, 2);
                              ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
                              p1和p3的构造方法里传的明显是两个不同内存地址的Color对象,就算ColorPoint没有继承Point类,你只要对两个不同ColorPoint对象中的Color属性传递不同内存地址的Color对象,你这样比较自然是永远返回false了,再比较之前就知道永远返回false了,还比较个啥啊.大哥,要比较引用类型是否相等,不是比较两个对象的内存地址的好不好,是要比较两个对象封装的属性值是否相等才靠谱的好不好.
                              没错:
                              Color c1 = Color.RED;
                              Color c2 = Color.RED;
                              c1和c2确实是指向同一个对象,不仅c1和c2的属性值完全一样,而且c1和c2在堆内存中的内存地址也是完全一致,所以它们就是指向同一个对象.就是什么原因造成的呢?请看Color类的源代码:
                              public final static Color red = new Color(255, 0, 0);
                              看到了吧,很清楚了吧,c1和c2的rgb的值不仅一样,c1和c2它们压根就是指向一个对象.比较c1和c2是否相等当然没有任何意义,因为c1和c2摆明了就是同一个对象,还比较个啥,在比较之前就知道永远返回true了还比较个啥.但是:
                              Color c3 = Color.RED;
                              Color c4 = Color.BLUE;
                              先看看Color.BLUE是怎么实例化的:
                              public final static Color blue = new Color(0, 0, 255);
                              明白了吧,调用Color.RED和调用Color.BLUE后实例化了两次Color对象,用c3和c4分别指向两个对象,换句话说,c3和c4是两个不同的对象,你用==去比较两个不同对象的内在地址是否相等?难道你认为这是有意义的?在比较之前就知道肯定返回false了,还比较个啥.
                              好了,现在关键点来了,前面说了这么多,就是为了给最后一段作铺垫的.为了让你彻底"醒悟",我只好多打些字了.
                              很简单,ColorPoint类根本就不应该用封装好的Color对象来作为"比较两个ColorPoint对象是否相等"的条件,因为Color类是单例设计,还比个神马东西.你完全可以只用ColorPoint类继承Point类而来的x和y属性来作为"比较两个ColorPoint对象是否相等"的条件,当然也可以在ColorPoint再封装其他类(前提是被ColorPoint类封装的类不能是单例设计,因为单例设计永远只返回一个对象,永远返回true的话还有比较的意义吗.还有就是被ColorPoint类封装的类必须要重写equals()方法,因为如果不重写equals()方法,就比较的是两个对象的内存地址是否相等了,这跟用==来比较没有任何的区别.).
                              明白了吗,我说的够清楚明白的吧,我相信你看到这里应该已经明白自己的错误了.


                              IP属地:辽宁15楼2012-06-04 13:08
                              回复