译者注:你可能会觉得Java很简单,Object的equals实现也会非常简单,但是事实并不是你想象的这样,耐心的读完本文,你会发现你对Java了解的是如此的少。如果这篇文章是一份Java程序员的入职笔试,那么不知道有多少人会掉落到这样的陷阱中。

  摘要

  本文描述重载equals方法的技术,这种技术即使是具现类的子类增加了字段也能保证equal语义的正确性。

  在《Effective Java》的第8项中,Josh Bloch描述了当继承类作为面向对象语言中的等价关系的基础问题,要保证派生类的equal正确性语义所会面对的困难。Bloch这样写到:

  除非你忘记了面向对象抽象的好处,否则在当你继承一个新类或在类中增加了一个值组件时你无法同时保证equal的语义依然正确。

  在《Programming in Scala》中的第28章演示了一种方法,这种方法允许即使继承了新类,增加了新的值组件,equal的语义仍然能得到保证。虽然在这本书中这项技术是在使用Scala类环境中,但是这项技术同样可以应用于Java定义的类中。在本文中的描述来自于Programming in Scala中的文字描述,但是代码被我从scala翻译成了Java

  常见的等价方法陷阱

  java.lang.Object 类定义了equals这个方法,它的子类可以通过重载来覆盖它。不幸的是,在面向对象中写出正确的equals方法是非常困难的。事实上,在研究了大量的Java代码后,2007 paper的作者得出了如下的一个结论:

  几乎所有的equals方法的实现都是错误的!

  这个问题是因为等价是和很多其他的事物相关联。例如其中之一,一个的类型C的错误等价方法可能意味着你无法将这个类型C的对象可信赖的放入到容器中。比如说,你有两个元素elem1和elem2他们都是类型C的对象,并且他们是相等,即elem1.equals(elm2)返回ture。但是,只要这个equals方法是错误的实现,那么你有可能会看见如下的一些行为:

Set hashSet = new java.util.HashSet();
hashSet.add(elem1);
hashSet.contains(elem2);    // returns false!

  当equals重载时,这里有4个会引发equals行为不一致的常见陷阱:

  1、定义了错误的equals方法签名(signature)Defining equals with the wrong signature.

  2、重载了equals的但没有同时重载hashCode的方法。Changing equals without also changing hashCode.

  3、建立在会变化字域上的equals定义。Defining equals in terms of mutable fields.

  4、不满足等价关系的equals错误定义Failing to define equals as an equivalence relation.

  在剩下的章节中我们将依次讨论这4中陷阱。

  陷阱1:定义错误equals方法签名(signature)

  考虑为下面这个简单类Point增加一个等价性方法:

public class Point {

    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    // ...
}

  看上去非常明显,但是按照这种方式来定义equals是错误的。

// An utterly wrong definition of equals
public boolean equals(Point other) {
  return (this.getX() == other.getX() && this.getY() == other.getY());
}