美文网首页Effective Java
覆盖equals时请遵守通用约定

覆盖equals时请遵守通用约定

作者: KubiL | 来源:发表于2017-05-05 12:47 被阅读0次

    不覆盖equals的几种情况

    1. 类的每个实例本质上都是唯一的(唯一了 用父类的equals判断就可以了)
    2. 不关心类是否提供了“逻辑相等”的测试功能(没有需要判断逻辑相等的需求)
    3. 超类已经覆盖了equals,从超类继承过来的行为对于子类也是合适的。(父类的equals对于子类够用了)
    4. 类是私有的或者是包级私有的,可以确定它的equals方法永远不会被调用。(这个类别人用不了,也确定了不会调用equals)

    为什么我们要覆盖equals

    当一个类具有“逻辑相等”的概念并且超类没有覆盖equals方法时,需要覆盖equals.通常这种是“值类”,即仅仅表示一个值得类。例如Integer,Date等。常常我们关心的是逻辑上是否是同一个对象 ,而不是是否指向同一个对象。逻辑相等还有一个好处是可以作为map的key或者集合set的元素。

    覆盖equals时需要遵循的约定(JavaSE6的规范)

    1. 自反性:对于任何非null的引用值x,x.equals(x)必须返回true
    2. 对称性: 对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true
    3. 传递性:对于任何非null的引用值,x,y,z,如果x.equals(y)为true,并且y.equals(z)也返回true,那么x.equals(z)也必须返回true
    4. 一致性:对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致地返回true,或者false
    5. 非空性:对于任何非null的引用值,x,x.equals(null)必须返回false

    自反性

    基本上很难违反这一条。

    对称性

    public final class CaseInsensitiveString{
        private final String s;
        public CaseInsensitiveString(String s){
            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;
        }
    }
    
    public class Test {
        public static void main(String[] args) {
            CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
            String s = "polish";
            System.out.println(cis.equals(s));
            System.out.println(s.equals(cis));
    
        }
    }
    

    这个类企图兼容与String能够做比较实则违反了对称性

    解决办法重写CaseInsensitiveString的equals方法
    @Override public boolean equals(Object o){
    //专一,只与CaseInsensitiveString类自己的实例比较
    return o instanceof CaseInsensitiveString 
           && ((CaseInsensitiveString)o).s.equalsIgnoreCase(s);
    }
    

    传递性

    public 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 p.x == x && p.y == y;
        }
    }
    
    public class ColorPoint extends Point{
        private final String color;
        public ColorPoint(int x, int y, String color){
            super(x, y);
            this.color = color;
        }
        @Override public boolean equals(Object o){
            if(!(o instanceof  ColorPoint)){
                return false;
            }
            return super.equals(o) && ((ColorPoint)o).color == color;
        }
    }
    
    Point p = new Point(1, 2);
    ColorPoint cp = new ColorPoint(1, 2, "red");
    

    此时违反了对称性

    然后我们修改了equals方法

    @Override public boolean equals(Object o){
            //非Point或ColorPoint
            if(!(o instanceof  Point)){
                return false;
            }
            if(!(o instanceof  ColorPoint)){//o为不带颜色的Point,使用Point的equals方法比较
                return o.equals(this);
            }
            //o为ColorPoint
            return super.equals(o) && ((ColorPoint)o).color == color;
        }
    
    public class Test {
        public static void main(String[] args) {
            ColorPoint p1 = new ColorPoint(1, 2, "red");
            Point p2 = new Point(1, 2);
            ColorPoint p3 = new ColorPoint(1, 2, "green");
            System.out.println(p1.equals(p2));
            System.out.println(p2.equals(p3));
            System.out.println(p3.equals(p1));
        }
    }
    

    这个是其中一种容易违反传递性的可能,即子类增加了新的属性,重写equals,从而影响了传递性。
    这个问题是面向对象语言中关于等价关系的一个基本问题: 无法在扩展可实例化的类的同时,既增加新的值组件,同时又保留equals的约定
    权宜之计:复合优先于继承

    public class ColorPoint{
        private final Point point;
        private final String color;
        public ColorPoint(int x, int y, String 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);
        }
    }
    

    一致性

    要保证相等的对象永远相等,不等的对象永远不等
    在equals方法中不要依赖不可靠的资源,例如java.net.URL的equals方法依赖与对URL中主机IP地址的比较,但是网络中主机IP地址有可能是变化的,因此java.net.URL的equals方法难以保证一致性原则。

    非空性

    一般我们使用一个显示的空测试来避免抛出空指针异常:

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

    其实,在equals方法中,最终是要将待比较对象转换为当前类的实例,以调用它的方法或访问它的属性, 这样必须先经过instanceof测试,而如果instanceof的第一个参数为null,则不管第二个参数是那种类型都会返回false,这样可以很好地避免空指针异常并且不需要单独的null检测 。

    @Override public boolean equals(Object o){
        if(!(o instanceof  MyType)){
        return false;
    }
    MyType mt = (MyType)o;
    ……
    }
    

    实现高质量equals方法

    1、使用==操作符检查参数是否为这个对象的引用。(一种优化手段 ,如果引用同样的对象 就完全不用进行后续可能出现的复杂的判断)

    2、使用instanceof操作符检查参数是否为正确的类型。

    3、把参数转换成正确的类型。

    4、对于要比较类中的每个关键域,检查参数中的域是否与该对象中对应的域相匹配。

    5、编写完equals方法后需要测试是否满足对称性、传递性和一致性。

    最佳编程实践

    1、覆盖equals时总要覆盖hashCode

    2、不要企图让equals方法过于智能

    3、不要将equals声明中的Object对象替换为其他的类型,因为替换后只是重载Object.equals(Object o)而不是覆盖。

    相关文章

      网友评论

        本文标题:覆盖equals时请遵守通用约定

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