zoukankan      html  css  js  c++  java
  • 第8条:覆盖equals时请遵守通用约定

    第8条:覆盖equals时请遵守通用约定

    引言:尽管Object是一个具体类,但是设计它主要是为了拓展。它所有的非final方法(equals、hashCode、toString、clone和finalize)都有明确的通用约定(general contract),因为它们被设计成是要被覆盖(override)的。任何一个类,它在覆盖这些方法的时候,都有责任遵守这些通用的约定;如果不能做到这一点,其它依赖于这些约定的类(例如HashMap和HashSet)就无法结合该类一起正常运作。

    覆盖equals方法看起来似乎很简单,但是有很多覆盖方式会导致错误,并且后果非常的严重。最容易避免这类问题的办法就是补覆盖equals方法,在这种情况下,类的每个实例都只是与它自身相等。如果满足以下任何一个条件,这正是所期望的结果:

    (1)类的每个实例本质上都是唯一的。对于代表活动实体而不是值(value)的类来说,例如Thread。Object提供的equals实现对于这些类来说正是正确的行为。

     

     1 public class Singleton {
     2     private static Singleton singleton;
     3 
     4     private Singleton() {}
     5     
     6     public static Singleton getInstance() {
     7         if(singleton == null)
     8             singleton = new Singleton();
     9         return singleton;
    10     }
    11 }

     

    1 Singleton singleton1 = Singleton.getInstance();
    2 Singleton singleton2 = Singleton.getInstance();
    3 System.out.println(singleton1.equals(singleton2));
    true

    (2)不关心类是否提供了“逻辑相等(logical equality)”的测试功能。例如,java.util.Random覆盖了equals,以检查两个Random实例是否产生相同的随机数列,但是设计者并不认为客户需要或者期望这样的功能。在这样的情况下,从Object继承的equals实现就已经足够了。

    (3)超类已经覆盖了equals,从超类继承过来的行为对于子类也是合适的。例如,大多数的Set实现都从AbstractSet继承equals实现,List实现从equals实现,Map实现从AbstractMap继承equals实现。

    (4)类是私有的或者包级私有的,可以确定它的equals方法永远不会被调用。在这种情况下,无疑是应该覆盖equals的,以防止它被意外调用。

    1 @Override
    2 public boolean equals(Object obj) {
    3     throw new AssertionError(); //Method is never called
    4 }

    那么,什么时候应该覆盖Object.equals呢?如果类具有自己特定的“逻辑相等”概念(不等同于对象等同的概念),而且超类还没有覆盖equals以实现期望的行为,这时候我们就需要覆盖equals方法。这通常属于“值类(value class)”的情形。值类仅仅是一个表示值得类,例如Integer或者Date。程序员在利用equals方法来比较值对象的引用时,希望知道它们在逻辑上是否相等,而不是想了解它们是否指向同一个对象。为了满足程序员的这种需求,不仅必需覆盖equals方法,而且这样做也使得这个类的实例可以被用作映射表(map)的键(key),或者集合(set)的元素,使映射或者集合表现出预期的行为。

    有一种“值类”不需要覆盖equals方法,即用例受控确保“每个值至多只存在一个对像”的类,枚举类型就属于这种类。对于这样的类而言,逻辑相同与对象等同是同一个概念,因此Object的equals方法等同于逻辑意义上的equals方法。

    在覆盖equals方法的时候,你必须要遵守它的通用约定。下面是约定的内容:

    equals方法实现了等价关系(equivalence relation):

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

    对于上述性质的部分解释

    自反性:对象必须等于其自身

    违反对称性:

     1 public class CaseInsensitiveString {
     2     private final String s;
     3     public CaseInsensitiveString(String s) {
     4         if(s == null)
     5             throw new NullPointerException();
     6         this.s = s;
     7     }
     8     /*
     9      * 1.equals()函数:主要是区分“比较的字符串” 大小写和长度时候相同,比较的类型可以是Object类型。
    10      * 2.equalsIgnoreCase()函数:比较的参数只能是字符串,这里只要字符串的长度相等,字母的大小写是
    11      * 忽略的。认为A-Z和a-z是一样的。
    12      */
    13     @Override        //违反对称性
    14     public boolean equals(Object o) {
    15         if(o instanceof CaseInsensitiveString)
    16             return s.equalsIgnoreCase(
    17                     ((CaseInsensitiveString) o).s);
    18         if(o instanceof String)
    19             return s.equalsIgnoreCase((String) o);
    20         return false;
    21     }
    22 }

    在这个类中,equals方法的意图非常好,它企图与普通的字符串(String)对象进行互操作。假设我们有一个不区分大小写的字符串和一个普通的字符串:

    1 CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
    2 String s = "polish";
    3 System.out.println(cis.equals((Object)s));
    true

    正如所料,cis.equals((Object)s)返回为true,虽然CaseInsensitiveString类中的equals方法知道普通的字符串(String)对象,但是String中的equals方法却并不知道不区分大小写的字符串,因此s.equals((Object)cis)返回为false,显然违反了对称性。假设你把不区分大小写的字符串放在一个集合中:

    1 List<CaseInsensitiveString> list = new ArrayList<CaseInsensitiveString>();
    2 list.add(cis);

    此时的list.contains(s)会返回什么结果呢?在Sun当前的实现中,它碰巧返回false,但是只是这个特定的实现得出的结果而已。在其它的实现中,它有可能返回true,或者抛出一个运行时(runtime)异常。一旦违反了equals约定,当其他对象面对你的对象时,你完全不知道这些对象的行为会怎么样。

    为了解决这个问题,只需把企图与String互操作的这段代码从equals方法找中去掉就可以了。这样做之后,就可以重构该方法,使它变成一条单独的返回语句:

    1 public static void main(String[] args) {
    2     CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
    3     String s = "polish";
    4     System.out.println(cis.equals((Object)s));
    5 }

     传递性:子类增加的信息会影响到equals的比较结果。我们首先以一个简单的不可变的二维整数型Point类作为开始:

     1 public class Point {
     2     private final int x;
     3     private final int y;
     4     public Point(int x, int y) {
     5         this.x = x;
     6         this.y = y;
     7     }
     8     @Override
     9     public boolean equals(Object obj) {
    10         if(!(obj instanceof Point))
    11             return false;
    12         Point p = (Point)obj;
    13         return p.x == x && p.y == y;
    14     }
    15 }

    假设你想要扩展这个类,为一个点添加颜色信息:

    1 public class ColorPoint extends Point {
    2     private final Color color;
    3 
    4     public ColorPoint(int x, int y, Color color) {
    5         super(x, y);
    6         this.color = color;
    7     }
    8 }

    equals方法会怎么样呢?如果完全不提供equals方法,而是直接从Point继承过来,在equals做比较的时候颜色信息就被忽略掉了。这样做虽然不会违反equals的约定,但是很明显这是无法接受的。假设你编写了一个equals方法,只有当它的参数是一个有色点,并且具有相同的位置和颜色时,它才会返回true:

    1 @Override
    2 public boolean equals(Object obj) {
    3     if(!(obj instanceof ColorPoint))
    4         return false;
    5     return super.equals(obj) && ((ColorPoint) obj).color == color;
    6 }

    这个方法的问题在于,你在比较普通点和有色点,以及相反的情形时,可能会得到不同的结果。前一种比较忽略了颜色信息,而后一种比较则总是返回false,因为参数的类型不正确。为了直观的说明问题所在,我们创建一个普通点和有色点:

    1 Point p = new Point(1,2);
    2 ColorPoint cp = new ColorPoint(1,2,Color.RED);
    3 System.out.println(p.equals(cp));
    4 System.out.println(cp.equals(p));

    运行结果如下:

    true
    false

    你可以做这样的尝试来修正这个问题,让ColorPoint.equals在进行“混合比较”时忽略颜色信息:

     1 @Override
     2 public boolean equals(Object obj) {
     3     if(!(obj instanceof Point))
     4         return false;
     5     //如果obj是一个普通点,则比较时忽略颜色信息
     6     if(!(obj instanceof ColorPoint))
     7         return obj.equals(this);
     8     //如果obj是一个有色点,进行全面比较
     9     return super.equals(obj) && ((ColorPoint) obj).color == color;
    10 }

    这种方法确实提供了对称性,但是却牺牲了传递性:

    1 ColorPoint p1 = new ColorPoint(1,2,Color.RED);
    2 Point p2 = new Point(1,2);
    3 ColorPoint p3 = new ColorPoint(1,2,Color.BLUE);
    4 System.out.println(p1.equals(p2));
    5 System.out.println(p2.equals(p3));
    6 System.out.println(p1.equals(p3));

    运行结果如下:

    true
    true
    false

    很显然,这违反了传递性,前两种比较不考虑颜色信息(色盲),而第三种比较则考虑了颜色信息。

    怎么解决呢?事实上,这是面向对象语言中关于等价关系的一个基本问题。我们无法在拓展可实例化的类的同时,既增加新的值组件,同时又保留equals约定,除非愿意放弃面向对象的抽象所带来的优势。

    你可能听说,在equals方法中庸getClass()测试代替instanceof测试,可以扩展可实例化的类和增加新的值组件,同时保留equals约定:

    1 @Override
    2 public boolean equals(Object obj) {
    3     if(obj == null || obj.getClass() != getClass())
    4         return false;
    5     Point p = (Point) obj;
    6     return p.x == x && p.y == y;
    7 }

    这段程序只有当对象具有相同的实现时,才能使对象等同。虽然这样也不算太糟糕,但是结果却是无法接受的。

    假设我们要编写一个方法,以检验某个整值点是否处在单位元中。下面是可以采用的其中一种方法:

     1 private static final Set<Point> unitCircle;
     2 static {
     3     unitCircle = new HashSet<Point>();
     4     unitCircle.add(new Point(1,0));
     5     unitCircle.add(new Point(0,1));
     6     unitCircle.add(new Point(-1,0));
     7     unitCircle.add(new Point(0,-1));
     8 }
     9 public static boolean onUnitCircle(Point p) {
    10     return unitCircle.contains(p);
    11 }

    虽然这可能不是实现这种功能的最快方法,不过它的效果很好。但是假设你通过某种不添加值组件的方式扩展了Point,例如让它的构造器记录创建了多少个实例:

    1 public class CounterPoint extends Point {
    2     private static final AtomicInteger counter = 
    3             new AtomicInteger();
    4     public CounterPoint(int x,int y) {
    5         super(x,y);
    6         counter.incrementAndGet();
    7     }
    8     public int numberCreated() {return counter.get();};
    9 }

     虽然没有一种令人满意的办法可以既扩展不可实例化的类,有增加组件,但还是有一种不错的权宜之计(workaround):复合优先于继承,我们不在让ColorPoint扩展Point,而是在ColorPoint中加入一个私有的Point域,以及一个公有视图(view)方法,此方法返回一个与该有色点处在相同位置的普通Point对象:

     1 public class ColorPoint {
     2     private final Color color;
     3     private final Point point;
     4     public ColorPoint(int x,int y,Color color) {
     5         if(color == null)
     6             throw new NullPointerException();
     7         point = new Point(x,y);
     8         this.color = color;
     9     }
    10     //返回这个颜色点的视图
    11     public Point asPoint() {
    12         return point;
    13     }
    14     @Override
    15     public boolean equals(Object obj) {
    16         if(!(obj instanceof ColorPoint))
    17             return false;
    18         ColorPoint cp = (ColorPoint) obj;
    19         return cp.point.equals(point) && cp.color.equals(color);
    20     }
    21 }

    注意:(1)在Java的平台库中,有一些类扩展了可实例化的类,并添加了新的值组件。例如,java.sql.Timestamp对java.util.Date进行了扩展,并增加了nanoseconds域。Timestamp的equals确实违反了对称性,所以在平时的代码编写中尽量不要将这两者混合在一起,否则出现问题会很难调试。

    (2)你可以在一个抽象(abstract)类的子类中增加新的值组件,而不会违反equals约定。

    一致性:如果两个对象相等,那就必须始终保持相等,除非它们中有一个对象被修改了。无论类是否是不可变的,都不要使equals方法依赖于不可靠的资源,如果违反了这条禁令,要想满足一致性的要求就非常困难了。

    非空性:指所有的对象都必须不等于null。许多类的equals方法都通过一个显示的null测试来防止这种情况:

    1 @Override
    2 public boolean equals(Object obj) {
    3     if(obj == null)
    4         return false;
    5     ...6 }

    这项测试是不必要的,为了测试其参数的等同性,equals方法必须先把参数转换成适当的类型,以便调用它的访问方法(accessor),或者访问它的域。转换之前,需要用instanceof操作符,检查其参数是否为正确的类型:

    1 @Override
    2 public boolean equals(Object obj) {
    3     if(!(obj instanceof MyType))
    4         return false;
    5     MyType mt = (MyType) obj;
    6     ...
    7 }

    结合这些所有的要求,得出了以下实现高质量equals方法的诀窍:

    1、使用==操作符检查“参数是否为这个对象的引用”。

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

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

    4、对于该类中的每个“关键(significant)”域,检查参数中的域是否与该对象中对应的域相匹配。

    5、当你编写完了equals方法之后,应该问自己三个问题:它是否是对称的、传递的、一致的,并且编写单元测试对其进行检查。

    6、覆盖equals时总要覆盖hashCode

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

    8、不要将equals声明中的Object对象替换成其它的类型。

  • 相关阅读:
    通讯录封装实现
    简单通讯录的实现 main..h .m文件全部
    iOS 开发 OC编程 字典和集合 排序方法
    iOS 开发 OC编程 数组冒泡排序.图书管理
    iOS 开发 OC编程 属性和字符串练习
    iOS 开发 OC编程 属性和字符串
    iOS 开发 OC编程 便利构造器以及初始化方法
    iOS 开发 OC编程 方法的书写
    IOS 开发 OC编程 类和对象
    iOS 开发 c语言阶段考试题
  • 原文地址:https://www.cnblogs.com/remote/p/10099247.html
Copyright © 2011-2022 走看看