Effective Java Item8-在覆盖equals(Object类的nonfinal方法)时遵循接口规范

Effective Java 2nd Edition Reading Notes

Item8: Obey the general contracts when overriding equals.

在覆盖equals(Object类的nonfinal方法)时遵循接口规范

 

覆盖equals方法开起来很容易,但是实际上覆盖错误的情况有很多中情况,其结果是极其严重的。防止覆盖错误最好的办法就是不去覆盖它,听起来很像废话,当一个类满足如下条件时,就不应该去覆盖equals方法,而是每个实例只与其自身equals

类的每个实例本身就是唯一的。例如Thread类是用于表示活跃对象,而不是值。此时使用Objectequals方法就可以了。

类不需要进行逻辑上的equals测试。例如可以通过覆盖equals方法来确认两个Random实例是否产生相同的随机序列数,但是这样的测试是没有必要的。此时Object提供的equals方法就是合适的。

超类已经覆盖了equals方法,而这个equals操作适用于子类。

  私有类或者package私有类,其equals方法不会被调用。

 

那么什么时候应该去覆盖Object.equals呢? 只有当类具有明显的逻辑相等的概念,而不是identity相同, 并且其父类没有覆盖Object.equals来实现相应的操作。一般说来,这样的类叫做值相关类,例如IntegerDateString等等。它们的equals方法用于判断它们是否逻辑上相等,而不是去判断它们是否引用同一个对象。

 

覆盖equals方法不仅上面的值相等判断,还影响到MapkeySet的元素(Set不包含重复元素)等等。

 

一种不需要覆盖equals方法的值相关类就是Item1中提到的使用工厂方法创建的针对特定的值只有一个对象。Enum类型也属于这种情况。在这种情况下,equals方法和==是一样的。例如下面的代码中的Book类就没有必要实现equals方法,因为针对不同的type,返回的Book实例都是同一个。此时下面的判断均返回true

Book scienceBookOne = Book.getInstance("science");

Book scienceBookTwo = Book.getInstance("science");

System.out.println(scienceBookOne.equals(scienceBookTwo));//true

System.out.println(scienceBookOne == scienceBookTwo);      //true

 

OverrideEquals.java

package com.googlecode.javatips4u.effectivejava.object;

import java.util.HashMap;

import java.util.Map;

public class OverrideEquals {

       public static class Book {

              private String title = null;

              private static final Map<String, Book> repository = new HashMap<String, Book>();

              static {

                     repository.put("science", new Book("Science Top 100"));

                     repository.put("computer", new Book("Effective Java"));

                     repository.put("math", new Book("Fibinacci"));

              }

              public Book(String title) {

                     this.title = title;

              }

              public static Book getInstance(String type) {

                     if (type == null) {

                            return null;

                     } else {

                            return repository.get(type);

                     }

              }

              public String getTitle() {

                     return title;

              }

       }

}

 

在覆盖equals方法的时候,需要遵循接口的通用规范:

public boolean equals(Object obj)

判断其它对象是否和当前对象”equal”

equals方法对非null引用实现对等性关系。

自反性:对于任意非null引用值xx.equals(x)返回true

对称性:对于任意非null引用值xy,当且仅当y.equals(x)返回truex.equals(y)返回true

传递性:对于任意非null引用值x,yz,如果x.equals(y)返回truey.equals(z)返回true,那么x.equals(z)也返回true

连续性:对于任意非null引用值xy,多次调用x.equals(y)要么总是返回true,要么总是返回false。假设两个对象的信息都没有变更。

对于任意非null引用xx.equals(null)返回false

Objectequals方法实现了最严格的相等性判断,即对于任意非null引用xyx.equals(y)返回true当且仅当xy引用相同的对象,即x==y返回true

注意通常情况下如果equals方法被覆盖,那么hashCode方法也需要被覆盖以便维护hashCode方法的通用规范:equal对象必须具有equal的哈希码。

 

自反性是指一个实例要equals它本身。一般情况下很难违反这个规范。假设违反了,那么当你将一个实例添加到Collection时,Collectioncontains方法将返回false

public class Dog {

       @Override

       public boolean equals(Object obj) {

              return false;

       }

}

public static void main(String[] args) {

       List<OverrideEquals.Dog> dogs = new ArrayList<OverrideEquals.Dog>();

       Dog dog = new OverrideEquals().new Dog();

       dogs.add(dog);

       System.out.println(dogs.contains(dog));//false

}

ArrayList#contains:

    /**

     * Returns <tt>true</tt> if this list contains the specified element.

     *

     * @param elem element whose presence in this List is to be tested.

     * @return  <code>true</code> if the specified element is present;

     *         <code>false</code> otherwise.

     */

    public boolean contains(Object elem) {

       return indexOf(elem) >= 0;

    }

 

    /**

     * Searches for the first occurence of the given argument, testing

     * for equality using the <tt>equals</tt> method.

     *

     * @param   elem   an object.

     * @return  the index of the first occurrence of the argument in this

     *          list; returns <tt>-1</tt> if the object is not found.

     * @see     Object#equals(Object)

     */

    public int indexOf(Object elem) {

       if (elem == null) {

           for (int i = 0; i < size; i++)

              if (elementData[i]==null)

                  return i;

       } else {

           for (int i = 0; i < size; i++)

              if (elem.equals(elementData[i]))

                  return i;

       }

       return -1;

    }

违反对称性规范就比较容易了,例如下面的例子:

// Broken - violates symmetry!

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;

       }

}

 

public static void main(String[] args) {

       // violate symmetry

       CaseInsensitiveString cis = new OverrideEquals().new CaseInsensitiveString(

                     "CaseInsensitiveString");

       CaseInsensitiveString cis2 = new OverrideEquals().new CaseInsensitiveString(

                            "CaseInsensitiveString");

       System.out.println(cis.equals("CaseInsensitiveString"));//true

       System.out.println(cis.equals(cis2)); // true

       System.out.println("CaseInsensitiveString".equals(cis));

}

显然Stringequals方法没有实现可以和外部自定义类的equals判断,所以返回false

假如将CaseInsentiveString添加到Collection中,那么collection.contains(“CaseInsensitiveString”)会返回什么?无法判定,现在的SunJVM实现是返回false的,如下所示,Suncontains实现是用输入参数去比较Collection中的元素。但是其他的JVM实现会返回什么就无法判定了。

    /**

     * Searches for the first occurence of the given argument, testing

     * for equality using the <tt>equals</tt> method.

     *

     * @param   elem   an object.

     * @return  the index of the first occurrence of the argument in this

     *          list; returns <tt>-1</tt> if the object is not found.

     * @see     Object#equals(Object)

     */

    public int indexOf(Object elem) {

       if (elem == null) {

           for (int i = 0; i < size; i++)

              if (elementData[i]==null)

                  return i;

       } else {

           for (int i = 0; i < size; i++)

              if (elem.equals(elementData[i]))

                  return i;

       }

       return -1;

    }

NOTE:一旦违反了equals的协议,那么无法预知其他对象如何处理你的对象。

解决上面问题的办法就是去掉和String的关联。

 

关于equals的传递性,请参考如下的代码:

public class Point {

              public int x;

              public int y;

              public Point(int x, int y) {

              }

              @Override

              public boolean equals(Object obj) {

                     if (obj instanceof Point) {

                            Point p = (Point) obj;

                            return p.x == x && p.y == y;

                     }

                     return false;

              }

       }

       public class ColorPoint extends Point {

              public int color;

              public ColorPoint(int x, int y, int color) {

                     super(x, y);

                     this.color = color;

              }

              @Override

              public boolean equals(Object obj) {

                     if (obj instanceof ColorPoint) {

                            return super.equals(obj) && ((ColorPoint) obj).color == color;

                     }

                     if (obj instanceof Point) {

                            return super.equals(obj);

                     }

                     return false;

              }

 

       }

在上面的代码中,ColorPoint继承了Point,并添加了本身都有的color属性。在super classequals方法中,比较了xy属性。而在sub class中,如果被比较的对象是super class的实例,那么就只比较xy(否则的话就违反了对称性),如果是sub class的实例,那么就比较xycolor

考虑如下的代码:

       // violate transitivity

       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)返回truep2.equals(p3)返回true,但是p1.equals(p3)返回false

问题的原因在于不应该去继承一个实例类,然后追加一个参与equals方法的值成员变量。

解决上面问题的一个好的方式是使用组合而非继承,将Point类的实例作为ColorPoint的一个成员。

// Adds a value component without violating the equals contract

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);

       }

}

但是,在JDK中,java.sql.Timestamp类就是继承了实例类java.util.Date,并且追加了扩展的value成员变量nanoseconds。并且Timestamp类的equals方法确实违反了对称性,并可能导致不可预知的后果。例如TimestampDate用在同一个Collection中,会导致不确定的结果。但是在Timestamp类中有明确的声明:不要将TimestampDate混合使用。

Note: This type is a composite of a java.util.Date and a separate nanoseconds value. Only integral seconds are stored in the java.util.Date component. The fractional seconds - the nanos - are separate. The Timestamp.equals(Object) method never returns true when passed a value of type java.util.Date because the nanos component of a date is unknown. As a result, the Timestamp.equals(Object) method is not symmetric with respect to the java.util.Date.equals(Object) method. Also, the hashcode method uses the underlying java.util.Date implementation and therefore does not include nanos in its computation.

Due to the differences between the Timestamp class and the java.util.Date class mentioned above, it is recommended that code not view Timestamp values generically as an instance of java.util.Date. The inheritance relationship between Timestamp and java.util.Date really denotes implementation inheritance, and not type inheritance.

java.util.Date#equals():

    public boolean equals(Object obj) {

        return obj instanceof Date && getTime() == ((Date) obj).getTime();

    }

java.sql.Timestamp#equals():

    public boolean equals(java.lang.Object ts) {

      if (ts instanceof Timestamp) {

       return this.equals((Timestamp)ts);

      } else {

       return false;

      }

    }

    public boolean equals(Timestamp ts) {

       if (super.equals(ts)) {

           if  (nanos == ts.nanos) {

              return true;

           } else {

              return false;

           }

       } else {

           return false;

       }

    }

              Date date = new Date();

              Timestamp timestamp = new Timestamp(date.getTime());

              System.out.println(date.equals(timestamp));//true

              System.out.println(timestamp.equals(date));//false

但是对于abstract的类来说,可以为其sub class追加value成员变量,而不必考虑违反对称性。因为没有办法显式的创建super class的实例,即super class的实例都是sub class类的实例。

例如java.lang.Number类,其子类如IntegerLong等都添加了value成员变量。例如上面的Point类如果声明为abstract类的话,就无法创建Point类本身的实例,只能创建子类的实例。即不会调用直接调用super classequals方法,而是调用子类型的equals方法。

 

一致性指的是如果两个对象equals,那么在它们中的一个或者同时被修改以前,要一直保持equals。也就是说可变(muable)对象可以和不同的对象equals,而不可变(immutable)对象却并不这样。不可变类例如String。通常情况下应该使用Object identity来判断equals

 

instance of操作符在第一个操作数为null的时候返回false

 

总结:实现equals方法的过程如下:

1.       使用==运算符来判断是否引用同一个对象。

2.       使用instance of操作符来判断参数是否是正确的类型。

3.       将参数cast成正确的类型。

4.       对于每个重要的属性,进行比较。如果步骤2中使用的是接口,那么必须使用接口中的方法访问属性,如果是类,那么用类进行访问属性。对于非floatdoubleprimitive类型,使用==判断等价性,对于float或者double类型,使用Float/DoublecompareTo判断等价性。对于对象引用,迭代的调用equals方法。对于数组类型,对其中的元素进行上述的操作。或者使用Arrays.equals方法(version 1.5)

 

以上步骤全部通过的话,返回true,否则返回false

当完成equals方法之后,确认是否满足自反,对称,传递和一致等规范。

一般来说,为了更好的性能,应该先比较可能不同的属性。

上述步骤的一个实例如下:PhoneNumberequals

@Override public boolean equals(Object o) {

       if (o == this){

              return true;

       }

       if (!(o instanceof PhoneNumber)){

              return false;

       }

       PhoneNumber pn = (PhoneNumber)o;

       return pn.lineNumber == lineNumber

       && pn.prefix  == prefix

       && pn.areaCode  == areaCode;

}

另外,与equals相关的内容还有如下:

1.       覆盖equals的时候同时覆盖hashCode方法。

2.       只进行简单的值判断,不要进行深层次的判断。例如对于File类,只要判断File的抽象路径名是否相同即可,而不需要判断他们的引用符号链接之类的深层次的相同。例如A->B,C->B但是ACabstract path name不同,但是equals返回true。不要做类似这样的判断。

3.       不要重载(overloads)方法。例如equals(MyClass instance)

例如:

package com.googlecode.javatips4u.effectivejava.object;

public class OverloadEquals {

       private int count;

       public OverloadEquals(int count) {

              this.count = count;

       }

       public boolean equals(OverloadEquals obj) {

              if (obj instanceof OverloadEquals) {

                     if (obj.count == this.count) {

                            return true;

                     }

              }

              return false;

       }

       /**

        * @param args

        */

       public static void main(String[] args) {

              OverloadEquals oe1 = new OverloadEquals(1);

              OverloadEquals oe2 = new OverloadEquals(1);

              Object oe3 = new OverloadEquals(1);

              System.out.println(oe1.equals(oe2));// invoke OverloadEquals#equals(OverloadEquals obj)

              System.out.println(oe1.equals(oe3));//return Object#equals(Object obj){return this == obj}

       }

}

可以通过使用@Override来避免这个错误的使用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值