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

一种Java版的规范的equals()

原文:A Canonical equals() For Java

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

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

// DefaultComparison.java

class 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:
true
false
*/

通常你想放开这种限制。典型的,如果两个对象具有相同类型,所有字段具有相同值,就可以认为这两个对象对等,但有时候你不想在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.java
import java.util.*;

interface EqualityFactory {
  Equality make(int i, String s, double d);
}

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

// Equality.java
import 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: false
Expected false, got false
-- Testing same object --
same object instanceof Equality: true
Expected true, got true
-- Testing different type --
different type instanceof Equality: false
Expected false, got false
-- Testing same values --
same values instanceof Equality: true
Expected true, got true
-- Testing different values --
different values instanceof Equality: true
Expected false, got false
*/

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

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

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

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

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

// SuccinctEquality.java
import 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: false
Expected false, got false
-- Testing same object --
same object instanceof Equality: true
Expected true, got true
-- Testing different type --
different type instanceof Equality: false
Expected false, got false
-- Testing same values --
same values instanceof Equality: true
Expected true, got true
-- Testing different values --
different values instanceof Equality: true
Expected false, got false
*/

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

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

// ComposedEquality.java
import 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: false
Expected false, got false
-- Testing same object --
same object instanceof Equality: true
Expected true, got true
-- Testing different type --
different type instanceof Equality: false
Expected false, got false
-- Testing same values --
same values instanceof Equality: true
Expected true, got true
-- Testing different values --
different values instanceof Equality: true
Expected false, got false
*/

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

子类间的比较

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

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

// SubtypeEquality.java
import 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.java
import 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 a752aeee
Pig2[1]: Ralph MEDIUM a752aeee
*/

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

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


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值