一种Java版的规范的`equals()`

来源:互联网 发布:don t starve mac 编辑:程序博客网 时间:2024/04/29 06:26

一种Java版的规范的equals()

原文:A Canonical equals() For Java

尽管有Java7种Objects.equals()方法的帮助,equals()方法仍然经常被写出冗余和混乱的范儿。本文将演示如何把equals()方法写得精炼到肉眼即可检查。

当你写一个类的时候,它自动继承Object类。如果你不重写equals()方法,你将默认使用Object.euqals()方法。它默认比较内存地址,所以只有当你比较 完全相同 的两个对象时,你才能得到true返回值。这种方案是“最有鉴别能力的”。

// DefaultComparison.javaclass DefaultComparison {  private int i, j, k;  public DefaultComparison(int i, int j, int k) {    this.i = i;    this.j = j;    this.k = k;  }  public static void main(String[] args) {    DefaultComparison      a = new DefaultComparison(1, 2, 3),      b = new DefaultComparison(1, 2, 3);    System.out.println(a == a);    System.out.println(a == b);  }}/* Output:truefalse*/

通常你想放开这种限制。典型的,如果两个对象具有相同类型,所有字段具有相同值,就可以认为这两个对象对等,但有时候你不想在equals()里比较某些字段。这是类设计流程的一部分。

一个恰当的equals()方法必须满足5个条件:

  1. 自反的:对任意xx.equals(x)应该返回true
  2. 对称的:对任意xyx.equals(y)返回true当且仅当y.equals(x)返回true
  3. 传递的:对任意xyz,如果x.equals(y)返回truey.equals(z)返回true,那么x.equals(z)应当返回true
  4. 一致的:对任意xy,只要用于比较的对象的信息没有更改,无论调用多少次x.equals(y),都一致的返回true或者一致的返回false
  5. 对任意非nullxx.equals(null)都返回false

下面是一些测试满足上述条件并判断你要比较的对象(测试里叫做rval)是否与当前对象对等:

  1. 如果rvalnull,不对等
  2. 如果rvalthis(你在用自己比较自己),对等
  3. 如果rval不是相同的类或其子类,不对等
  4. 如果以上测试全通过,你必须决定rval中哪些字段是重要的(并一致的),然后比较它们

Java7引入了Objects类来帮助这个流程,我们可以用它来写一个更好的equals()方法

下面的例子比较不同版本的Equality类。为防止重复代码,我们使用工厂方法来构建用例。这个EqualityFactory接口只简单的定义了一个make()方法来生成Equality对象,所以不同的EqualityFactory可以产生不同的Equality子类:

// EqualityFactory.javaimport java.util.*;interface EqualityFactory {  Equality make(int i, String s, double d);}

现在我们将定义Equality,它包含3个字段(我们认为在比较时它们全部重要)和euqals()方法满足上述的四项测试。构造器会输出类名,这样我们在测试时可以确保类型的正确:

// Equality.javaimport java.util.*;public class Equality {  protected int i;  protected String s;  protected double d;  public Equality(int i, String s, double d) {    this.i = i;    this.s = s;    this.d = d;    System.out.println("made 'Equality'");  }  @Override  public boolean equals(Object rval) {    if(rval == null)      return false;    if(rval == this)      return true;    if(!(rval instanceof Equality))      return false;    Equality other = (Equality)rval;    if(!Objects.equals(i, other.i))      return false;    if(!Objects.equals(s, other.s))      return false;    if(!Objects.equals(d, other.d))      return false;    return true;  }  public void  test(String descr, String expected, Object rval) {    System.out.format("-- Testing %s --%n" +      "%s instanceof Equality: %s%n" +      "Expected %s, got %s%n",      descr, descr, rval instanceof Equality,      expected, equals(rval));  }  public static void testAll(EqualityFactory eqf) {    Equality      e = eqf.make(1, "Monty", 3.14),      eq = eqf.make(1, "Monty", 3.14),      neq = eqf.make(99, "Bob", 1.618);    e.test("null", "false", null);    e.test("same object", "true", e);    e.test("different type", "false", new Integer(99));    e.test("same values", "true", eq);    e.test("different values", "false", neq);  }  public static void main(String[] args) {    testAll( (i, s, d) -> new Equality(i, s, d));  }}/* Output:made 'Equality'made 'Equality'made 'Equality'-- Testing null --null instanceof Equality: falseExpected false, got false-- Testing same object --same object instanceof Equality: trueExpected true, got true-- Testing different type --different type instanceof Equality: falseExpected false, got false-- Testing same values --same values instanceof Equality: trueExpected true, got true-- Testing different values --different values instanceof Equality: trueExpected false, got false*/

testAll()方法用我们能想到的所有不同类型的对象来执行比较。它用工厂构造Equality对象。

main()方法里,注意对testAll()方法调用的简化。因为EqualityFactory只有单个方法,可以使用兰布达表达式定义make()方法实现。

上面的equals()方法臃肿的令人心烦,幸好它可以被简化成一种规范的形式。经研究发现:

  1. 类型检查instanceOf消除了空值null检查的必要性
  2. this的比较是多余的,一个正确实现的equals()方法对自我比较一定没问题

因为&&是短路比较,当它首次遇到一个失败时会退出并返回一个false。所以,通过&&将这些检查串联起来,我们可以将equals()方法写得更精炼:

// SuccinctEquality.javaimport java.util.*;public class SuccinctEquality extends Equality {  public SuccinctEquality(int i, String s, double d) {    super(i, s, d);    System.out.println("made 'SuccinctEquality'");  }  @Override  public boolean equals(Object rval) {    return rval instanceof SuccinctEquality &&      Objects.equals(i, ((SuccinctEquality)rval).i) &&      Objects.equals(s, ((SuccinctEquality)rval).s) &&      Objects.equals(d, ((SuccinctEquality)rval).d);  }  public static void main(String[] args) {    Equality.testAll( (i, s, d) ->      new SuccinctEquality(i, s, d));  }}/* Output:made 'Equality'made 'SuccinctEquality'made 'Equality'made 'SuccinctEquality'made 'Equality'made 'SuccinctEquality'-- Testing null --null instanceof Equality: falseExpected false, got false-- Testing same object --same object instanceof Equality: trueExpected true, got true-- Testing different type --different type instanceof Equality: falseExpected false, got false-- Testing same values --same values instanceof Equality: trueExpected true, got true-- Testing different values --different values instanceof Equality: trueExpected false, got false*/

对每个SuccinctEquality,基类构造器先于衍生类构造器调用。输出显示我们得到的结果依然正确。你可以看到短路发生在空置null检测和“不同类型”检测,否则equals()方法里比较列表下面的测试将在类型转换时抛出异常。

当你用别的类来组装你的类时,Objects.euqals()就变得耀眼了:

// ComposedEquality.javaimport java.util.*;class Part {  String ss;  double dd;  public Part(String ss, double dd) {    this.ss = ss;    this.dd = dd;  }  @Override  public boolean equals(Object rval) {    return rval instanceof Part &&      Objects.equals(ss, ((Part)rval).ss) &&      Objects.equals(dd, ((Part)rval).dd);  }}public class ComposedEquality extends SuccinctEquality {  Part part;  public ComposedEquality(int i, String s, double d) {    super(i, s, d);    part = new Part(s, d);    System.out.println("made 'ComposedEquality'");  }  @Override  public boolean equals(Object rval) {    return rval instanceof ComposedEquality &&      super.equals(rval) &&      Objects.equals(part, ((ComposedEquality)rval).part);  }  public static void main(String[] args) {    Equality.testAll( (i, s, d) ->      new ComposedEquality(i, s, d));  }}/* Output:made 'Equality'made 'SuccinctEquality'made 'ComposedEquality'made 'Equality'made 'SuccinctEquality'made 'ComposedEquality'made 'Equality'made 'SuccinctEquality'made 'ComposedEquality'-- Testing null --null instanceof Equality: falseExpected false, got false-- Testing same object --same object instanceof Equality: trueExpected true, got true-- Testing different type --different type instanceof Equality: falseExpected false, got false-- Testing same values --same values instanceof Equality: trueExpected true, got true-- Testing different values --different values instanceof Equality: trueExpected false, got false*/

注意对super.equals()的调用——不必重新发明轮子(并且你并不总是有权限访问基类的所有必要组成部分)。

子类间的比较

继承暗示当两个不同的子类向上塑型时可能变得“对等”。假设你有一个Pet对象的集合,这个集合天然地接受Pet的子类:在这个例子里,可以是DogPig。每个Pet有一个namesize,还有一个内部唯一标识id

我们用Objects类来规范化的定义equals()hashCode()方法,但我们只在基类Pet里定义它们,并且它们都不包含id。从equals()方法的角度看,这意味着对象是否是Pet,而不关心它是哪个特定种类的Pet

// SubtypeEquality.javaimport java.util.*;enum Size { SMALL, MEDIUM, LARGE }class Pet {  private static int counter = 0;  private final int id = counter++;  private final String name;  private final Size size;  public Pet(String name, Size size) {    this.name = name;    this.size = size;  }  @Override  public boolean equals(Object rval) {    return rval instanceof Pet &&      // Objects.equals(id, ((Pet)rval).id) && // [1]      Objects.equals(name, ((Pet)rval).name) &&      Objects.equals(size, ((Pet)rval).size);  }  @Override  public int hashCode() {    return Objects.hash(name, size);    // return Objects.hash(name, size, id);  // [2]  }  @Override  public String toString() {    return String.format("%s[%d]: %s %s %x",      getClass().getSimpleName(), id,      name, size, hashCode());  }}class Dog extends Pet {  public Dog(String name, Size size) {    super(name, size);  }}class Pig extends Pet {  public Pig(String name, Size size) {    super(name, size);  }}public class SubtypeEquality {  public static void main(String[] args) {    Set<Pet> pets = new HashSet<>();    pets.add(new Dog("Ralph", Size.MEDIUM));    pets.add(new Pig("Ralph", Size.MEDIUM));    pets.forEach(System.out::println);  }}/* Output:Dog[0]: Ralph MEDIUM a752aeee*/

如果我们只考虑类型,那么它是有意义的——有时——只从它们的基类的立场来看,这正是 Liskov替换原则 的基础。这个代码很好的符合了这个原则,因为衍生类没有额外添加任何不在基类的方法。衍生类只在行为上不同,而不是在接口上(这当然不是通常的情况)。

但我们提供两个有着相同数据的不同的对象并把它们放到一个HashSet<Pet>,只有一个留存下来。这凸显了equals()方法不是一个完美的数学概念,而是(至少部分是)一种呆板的方法。在哈希化的数据结构里,hashCode()equals()必须密切相关的一同定义才能恰当的工作。

在上面的例子里,DogPigHashSet哈希化进了同一个篮子。在这里,HashSet依赖equals()方法来区分对象,但equals()认为这两个对象对等。HashSet不添加Pig因为它已经有一个相同对象了。

我们依然可以通过强制区分这两个原本相等的对象来使代码工作。这里,每个Pet已经有一个唯一id,所以你可以取消标记[1]处的注释或者将hashCode()方法切换成标记[2]的代码。在规范化方法里,你可以两者都做,在两个方法中引入所有“不变性”的字段(“不变性”使得equals()hashCode()在哈希化的数据结构里存储和返回时不会产生不同的值。我对“不变性”加引号是因为你必须评估是否会发生改变)。

补充说明:在hashCode()中,如果你只使用一个字段,用Objects.hashCode(),如果使用多个字段,用Objects.hash()

我们也可以通过在子类中遵循标准形式来定义equals()来解决这个问题(但还是不包含id):

// SubtypeEquality2.javaimport java.util.*;class Dog2 extends Pet {  public Dog2(String name, Size size) {    super(name, size);  }  @Override  public boolean equals(Object rval) {    return rval instanceof Dog2 &&      super.equals(rval);  }}class Pig2 extends Pet {  public Pig2(String name, Size size) {    super(name, size);  }  @Override  public boolean equals(Object rval) {    return rval instanceof Pig2 &&      super.equals(rval);  }}public class SubtypeEquality2 {  public static void main(String[] args) {    Set<Pet> pets = new HashSet<>();    pets.add(new Dog2("Ralph", Size.MEDIUM));    pets.add(new Pig2("Ralph", Size.MEDIUM));    pets.forEach(System.out::println);  }}/* Output:Dog2[0]: Ralph MEDIUM a752aeeePig2[1]: Ralph MEDIUM a752aeee*/

注意hashCode()方法是相同的,但两个对象已经不再对等了,所以都出现在HashSet里。并且,super.equals()意味着我们不必访问基类的私有字段。

对此的一种解释是Java通过对hashCode()equals()的定义分离了可替代性。我们依然可以把DogPig放进一个Set里而不管hashCode()equals()如何定义,但对象在哈希化的数据结构里不会表现正确,除非这些方法用哈希化结构在心里定义了。不幸的是,equals()不只是与hashCode()方法关联使用。这使得当你试图避免为某些类定义它时事情更复杂了,这就是规范化的价值了。但是,这也使事情进一步复杂了,因为有时你也不需要定义这些方法。


0 0