覆盖equals方法需要注意的

作者: DongBold | 来源:发表于2016-12-05 22:34 被阅读465次

    Effective java读书笔记 第八条

    覆盖equals方法其实是非常有讲究的, 许多覆盖方式会导致错误, 并且带来非常严重的后果. 首先看一下文档上对equals方法是怎么说的吧

    Object类的equals方法

    自反性, 对称性, 传递性, 有木有想起离散数学=-=

    首先看一下哪些情况是不用覆盖equals方法的, 这样的话, 类的每个实例都只与它自己相等.

    • 类的每个实例本质上都是唯一的
      什么意思呢, 就是说这个类表示活动实体, 而不是值, 就不需要去覆盖equals了, Object提供的实现就已经足够了. 比如说Thread, GUI的控件等等

    • 不关心类是否提供逻辑相等的测试功能
      比如说java.util.Random覆盖了equals, 用来检查两个Random实例产生的随机数序列是否相同, 其实这并没有什么卵用啊, Object对于equals的实现就够啦

    • 超类已经覆盖了equals, 对于子类也适用*
      这种情况无需再去实现equals

    如果类具有自己特有的逻辑相等概念, 而超类的的equals并不是期望的实现的话, 就需要我们去覆盖equals了.

    需要覆盖equals的时候就需要遵守上面截图的规范啦:

    • 自反性
      自反性说明一个对象是等于其自身, 自己和自己相等, Object的也就实现了这个了:
    publicbooleanequals(Objectobj){
    return(this==obj);
    }
    

    我就是我, 是颜色不一样的烟火

    • 对称性
      简单来说, 就是a=b成立的话, 那么b=a必定成立.
      来看个例子, 有一个CaseInsensitiveString类, 这个类比较的时候会忽略大小写:
    final class CaseInsensitiveString{
        private final String s;
        public CaseInsensitiveString(String s) throws NullPointerException{
            if (s == null) {
                throw new NullPointerException();
            }
            this.s = s;
        }
    
        @Override
        public boolean equals(Object o) {
            if(o instanceof CaseInsensitiveString) {
                return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
            } 
            if(o instanceof String) {
                return s.equalsIgnoreCase((String) o);
            }
            return false;
        }
    }
    

    覆盖了equals, 判断s与CaseInsensitiveString类的s或者和String忽略大小后是否相等, 否则就返回false, 然后我们来测试一下,

    CaseInsensitiveString cis1 = new CaseInsensitiveString("boom!");
            CaseInsensitiveString cis2 = new CaseInsensitiveString("Boom!");
            String s = "BoOM!";
            System.out.println(cis1.equals(cis2));
            System.out.println(cis1.equals(s));
        }
    

    输出正如我们所料, 都是true, 但是别忘了自反性啊, cis1.equals(s)输出true, s.equals(cis1)也应该是输出true的, 事实上s.equals(cis1)的结果是false. 显然违反了对称性, String类的equals并不知道不区分大小写的CaseInsensitiveString类, 因此s.equals(cis1)返回了false.
    为了解决这个问题, 只要将企图与String互操作的那段代码从equals中删除就行了

    @Override
        public boolean equals(Object o) {
            return o instanceof CaseInsensitiveString && ((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
        }
    
    • 传递性
      最复杂的就是传递性了, 离散中最麻烦的也是求传递闭包了.
      传递性的意思也很简单的, 就是a=b, b=c的话, 那么a和c也是相等的.

      有一个Point类, 用来表示坐标点
    class Point{
        private final int x;
        private final int y;
        
        public Point(int x, int y) {
            this.x = x;
            this.y = y;
        }
    
        @Override
        public boolean equals(Object o) {
            if (!(o instanceof Point)) {
                return false;
            }
            Point p = (Point)o;
            return x == p.x && y == p.y;
        }
    }
    

    然后又有一个ColorPoint类, 来表示带颜色的点

    class ColorPoint extends Point {
        private final Color color;
        public ColorPoint(int x, int y, Color color) {
            super(x, y);
            this.color = color;
        }
    }
    

    没有复写equals的情况下, 虽然ColorPoint和Point作比较的时候能饭后正确的结果, 但是两个ColorPoint之间做比较的时候忽略了颜色信息, 这显然不是我们想要的结果, 于是乎:

    @Override
        public boolean equals(Object o) {
            if (!(o instanceof ColorPoint)) {
                return false;
            }
            return super.equals(o) && ((ColorPoint) o).color == color;
        }
    }
    

    这样对了吗? 抱歉, 问题还是很大, 虽然实现了两个有色点之间的比较, 但是当普通点和有色点比较的时候, 违反了对称性, 普通点和有色点比较会忽略颜色, 而有色点和普通点则总是返回false.
    继续改:

    @Override
        public boolean equals(Object o) {
            if (!(o instanceof Point)) {
                return false;
            }
            if(!(o instanceof ColorPoint)) {
                return o.equals(this);
            }
            return super.equals(o) && ((ColorPoint) o).color == color;
        }
    

    如果o不是ColorPoint, 就用o去比较this, 这样就会忽略颜色信息了, 测试一下:

            ColorPoint p1 = new ColorPoint(1, 1, Color.RED);
            Point p2 = new Point(1, 1);
            ColorPoint p3 = new ColorPoint(1, 1, Color.GREEN);
            System.out.println(p1.equals(p2));
            System.out.println(p2.equals(p3));
    

    返回的都是true, 很好, 对称性的问题解决了, 等等, 这里不是在讨论传递性吗!!!按照传递性来说, p1=p2, p2=p3, 所以p1和p3肯定是相等啊喂, 但是这里很明显就是不相等的, 大家又不是色盲.
    这样写呢

    @Override
        public boolean equals(Object o) {
            if (o == null || o.getClass() != getClass()) {
                return false;
            }
            Point p = (Point)o;
            return x == p.x && y == p.y;
        }
    

    只有当对象相同时才 比较, 这样虽然解决了问题, 但是却不是我们想要的解决方法, 来看一个更好的实现, 用复合代替继承:

    class ColorPoint {
        private final Color color;
        private final Point point;
        public ColorPoint(int x, int y, Color color) {
            if (color == null) {
                throw new NullPointerException();
            }
            point = new Point(x, y);
            this.color = color;
        }
    
        public Point asPoint() {
            return point;
        }
    
        @Override
        public boolean equals(Object o) {
            if(!(o instanceof ColorPoint)) {
                return false;
            }
            ColorPoint cp = (ColorPoint) o;
            return cp.point.equals(point) && cp.color.equals(color);
        }
    }
    
    • 一致性
      如果两个对象是相等的, 就应该保持一直是相等的, 除非这两个对象中有一个或者两个都被修改了, 所以记住: 相等的对象永远相等, 不相等的对象永远不相等.

    • 非空性
      所有的对象都不能为null, 尽管很难想象什么情况下o.equals(null)会返回true. 但是意外抛出NullPointerException异常的可能却不难想象, 所以可以这样写来不允许抛出NullPointerException异常

    @Override
    public boolean equals(Object o) {
        if (o == null) {
            return false;
        }
    }
    

    相关文章

      网友评论

        本文标题:覆盖equals方法需要注意的

        本文链接:https://www.haomeiwen.com/subject/rrdlmttx.html