Item 8 覆盖equals时请遵守通用约定

在覆盖equals方法的时候,你必须要遵守它的通用约定,不遵守,写出来的方法,会出现逻辑错误。下面是约定的内容:

 
equals方法实现了等价关系:
 
  • 自反性。对于任何非null的引用值,x.equals(x)必须返回true。
  • 对称性。对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true。
  • 传递性。对于任何非null的引用值x,y和z,如果x.equals(y)返回true,并且y.equals(z)也返回true,那么x.equals(z)也必须返回true。
  • 一致性。对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致地返回true,或者一致地返回false。
  • 对于任何非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;
  }

  // Broken - violates symmetry!
  @Override
  public boolean equals(Object o) {
    if (o instanceof CaseInsensitiveString)
      return s.equalsIgnoreCase(((CaseInsensitiveString) o). s);
    if (o instanceof String) // One-way interoperability!
      return s.equalsIgnoreCase((String) o);
    return false;
  }

  // This version is correct.
  // @Override public boolean equals(Object o) {
  // return o instanceof CaseInsensitiveString &&
  // ((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
  // }

  public static void main(String[] args) {
    CaseInsensitiveString cis = new CaseInsensitiveString("Polish" );
    String s = "polish";
    System.out.println(cis.equals(s) + "  " + s.equals(cis));
  }
}
-------------------------
true  false
 
上述代码的问题,要通过写测试来发现。写测试的时候,按照对称性规则来写。对称性规则就是  x.equals(y)为true, y.equals(x)也要为true。
如果,没有按照规则来写单元测试,那么,是不容易发现上述代码存在的问题的。关键就是,按照规则,实现了代码之后,要写单元测试。
 
传递性
 
public class ColorPoint extends Point {
    private final Color color ;

    public ColorPoint( int x, int y, Color color) {
        super(x, y);
        this.color = color;
    }

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

    // Broken - violates transitivity!
    // @Override public boolean equals(Object o) {
    // if (!(o instanceof Point))
    // return false;
    //
    // // If o is a normal Point, do a color-blind comparison
    // if (!(o instanceof ColorPoint))
    // return o.equals(this);
    //
    // // o is a ColorPoint; do a full comparison
    // return super.equals(o) && ((ColorPoint)o).color == color;
    // }

    public static void main(String[] args) {
        // First equals function violates symmetry
        Point p = new Point(1, 2);
        ColorPoint cp = new ColorPoint(1, 2, Color.RED);
        System. out.println(p.equals(cp) + " " + cp.equals(p));

        // Second equals function violates transitivity
        ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
        Point p2 = new Point(1, 2);
        ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
        System. out.printf("%s %s %s%n" , p1.equals(p2), p2.equals(p3),
                p1.equals(p3));
    }
}
-----------------------------------------------------
结果:
true false
false true false
 
同理,使用传递性规则,写个测试,就可以发现问题。
 
解决办法:
使用复合,避免使用继承。也就是,在实现ColorPoint时,不继承Point,而在ColorPoint中放一个Point类型的成员。然后,所有ColorPoint都有Point类成员,Color成员,比较两个ColorPoint就是比较这两个成员的值。而不是跟上面那样,因为使用了继承,ColorPoint可以和Point进行比较。
 
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;
       }

        // See Item 9
        @Override
        public int hashCode() {
               return 31 * x + y ;
       }
}

public enum Color {
        RED, ORANGE , YELLOW, GREEN, BLUE, INDIGO , VIOLET
}

public class ColorPoint {
        private final Point point ;
        private final Color color ;

        public ColorPoint(int x, int y, Color color) {
               if (color == null)
                      throw new NullPointerException();
               point = new Point(x, y);
               this.color = color ;
       }

        /**
        * Returns the point -view of this color point.
        */
        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);
       }

        @Override
        public int hashCode() {
               return point .hashCode() * 33 + color.hashCode();
       }
}
一致性
 
“ 如果两个对象相等,它们就必须始终保持相等,除非它们中有一个对象(或者两个都)被修改了。换句话说,可变对象在不同的时候可以与不同的对象相等,而不可变对象则不会这样。”
 
“无论类是否是不可变的,都不要使equals方法依赖于不可靠的资源。如果违反了这条禁令,要想满足一致性的要求就十分困难了。”比如,比较的时候,依赖解析到的IP地址:
 
  1. @Override
  2. public boolean equals(Object obj) {
  3. if (!(obj instanceof InetAddress)) {
  4. return false;
  5. }
  6. return Arrays.equals(this.ipaddress, ((InetAddress) obj).ipaddress);
  7. }
在比较两个URL是否相等时,java.net.URL使用了这样的依据,两个URL对应的IP地址是否是相等的,如果相等,则它们两个是同一个URL。它这个实现,依赖了不可靠的资源,IP地址。IP地址,随着时间的推移,当时相等的两个URL,会不相等,因为,它们在比较的时候,还要解析IP地址。
 
非空性
 
是指比较的两个对象,不能为空。但是,在重写equals方法时,无需使用 if( o == null ) return false。因为,测试两个对象是否相等时,可以直接使用instanceof运算符:
	* 
@Override
	*  public boolean equals(Object obj) {
	*  if (!(obj instanceof InetAddress)) {
	*  return false;
	*  }
	*  return Arrays.equals(this.ipaddress, ((InetAddress) obj).ipaddress);
	*  }  
instanceof运算符,包含了null的检测,也就是说,obj如果为空,那么 obj instanceof InetAddress是false的,这样就不会进行具体的比较,直接返回false.
 
来自《effective java》中的诀窍,实现高质量的equals的诀窍:
 
1.使用 == 操作符检查 “参数是否为这个对象的引用”。如果是,则返回true。这只不过是一种性能优化,如果比较操作有可能很昂贵,就值得这么做。使用==操作符,将当前独享的引用与参数进行比较。
 
 
2.使用instanceof操作符检查”参数是否为正确的类型”。如果不是,则返回false。一般说来,所谓“正确的类型”是指equals方法所在的那个类。有些情况下,是指该类所实现的某个接口。
 
3.把equals方法传入的参数转换成正确的类型。
 
4.对于该类中的每个“关键(significant)”域,检查参数中的域是否与该对象中对应的域相匹配。如果这些测试全部成功,则返回true;否则返回false。
 
5.对于float,double类型的关键域,使用Float.compare,Double.compare来进行比较,而不使用==,这是因为存在Float.NaN,-0.0f以及类似的double常量。
 
6.对于关键域的类型是非float,double类型的基本类型,使用==进行比较。
--------------------------------------------------------------------------------------
注意:
 
1.覆盖equals时总要覆盖hashCode方法,这是为了让该类可以在使用了散列的集合中使用,比如Map.
 
2.不要将equals声明中的Object对象替换为其它的类型。因为,我们是在覆盖java.lang.object的equals方法,而不是重载。覆盖表示,某个方法,在子类表现的行为是不同的;而重载,则是提供了两种行为,一个方法。
 
3.不要企图让equals方法过于智能。如果只是简单地测试域中的值是否相等,则不难做到准守equals约定。如果想过度地去寻求各种等价关系,则很容易陷入麻烦之中。