ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

equals() 和 hashCode() 实现有什么问题?有什么解决办法?

2021-05-30 10:56:51  阅读:218  来源: 互联网

标签:解决办法 Object equivalent Equivalence equals hashCode other


本文介绍了 `equals()` 和 `hashCode()` 实现的常见问题,并提出了 Equivalence API 作为一种解决办法。


背景


要正确实现 `equals()` 和 `hashCode()` 需要太多繁文缛节。


不仅实现起来费时费力,维护成本也很高。虽然 IDE 可以帮助生成初始代码,但是当类发生变化时,还是需要阅读、调试这些代码。随着时间推移,这些方法会成为隐蔽的 bug(详见附录 bug 列表)。


以下面这个普通的 `Point` 类为例,展示了如何正确实现 `equals()` 和 `hashCode()`:


```java
class Point {
 final int x;
 final int y;

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

 @Override public boolean equals(Object other) {
   if (!(other instanceof Point)) {
     return false;
   }
   Point that = (Point) other;
   return x == that.x && y == that.y;
 }

 @Override public int hashCode() {
   return Objects.hash(x, y);
 }
}
```


目标


本文中的提案旨在创建一个可读性强、功能正确、高效的 `equals()` 和 `hashCode()` 开发库。


次要目标,为已定义类型提供一种新的 `equals()` 和 `hashCode()` 等价(equivalence)定义。API 接口中的方法用来进行等价性测试并计算每个实例的 hashCode。


“警告:示例 API 的所有细节都非最终版本,只为展示提案的核心功能。”


```java
interface Equivalence<T> {
 boolean equivalent(T self, Object other);
 int hash(T self);
}
```


使用这个“假想” API 后 `Point` 代码会变成下面这样:


```java
class Point {
 int x;
 int y;

 private static final Equivalence<Point> EQ =
     Equivalence.of(Point.class, p -> p.x, p -> p.y);

 @Override public boolean equals(Object other) {
   return EQ.equivalent(this, other);
 }

 @Override public int hashCode() {
   return EQ.hash(this);
 }
}
```


未来,类似 `Point` 这样值类(value class)有希望成为 [record][1] 这样的数据类(data class)。但总会有一些情况会要求实现 `equals` 和 `hashCode`,无法转化为 record。本文的提案是对 `record` 一种友好的补充,有助于避免手工实现 `equals` 和 `hashCode`。


> 译注:`record` 是 Brian Goetz 在 2019.2 提出的一种数据类 Data Class。类似 Kotlin 中的 data class。


[1]:https://cr.openjdk.java.net/~briangoetz/amber/datum.html


哪些不是本文的目标


要达成目标还可以增加语言扩展或编译器支持,也许性能上会更好,但这些不属于本文的讨论范围。本文的目标是通过开发库来解决并达到最佳效果。


Java 未来可能支持“字段引用(field reference)”,比如 `Foo::x` 这里 `x` 表示一个字段。Equivalence API 很好地契合了这个新特性并提供支持。但是新特性的细节不在本文的讨论范围内。


需求


API 是否应该同时支持 equals() 和 hashCode(),还是只支持 equals()?

同时支持 `equals()` 和 `hashCode()` 的优点在于可以避免开发中经常遇到的 bug。那种认为 `hashCode` 的实现比 `equals` 更可靠的观点是不正确的。在 `equals()` 和 `hashCode()` 实现中采取单一规范的状态列表不仅能减少样板代码,更是关乎代码的正确性。


(与 `Comparator` 共享 `equals()` 和 `hashCode()` 的状态是很有意思的一件事情。相关内容参见下文“与 Comparator 的关系”)


> 译注:这里的状态 state,可简单理解为对象中的属性。


API 是否应该支持自定义比较函数?


API 可以一直使用 `Object.equals` 和 `Object.hashCode`,也可以采用与状态相关的自定义 `comparator` 实现。例如,在比较 `String` 字段时要求不区分大小写。


```java
private static final Equivalence<Point> EQ =
   Equivalence.forClass(Person.class)
       .andThen(Person::age)
       .andThen(Person::name, CASE_INSENSITIVE_EQ); // 也是 Equivalence 类型
```


(使用自定义 `comparator` 的另一个例子是数组。通常会用 `Arrays.deepEquals` 和 `Object.deepHashCode` 替换 `Object.equals` 和 `Object.hashCode`。由于数组是一种常见数据结构,在 API 中优先考虑数组是很自然的事情。下面会对此进行详细讨论)


在 hashCode 实现中忽略一些状态?


`hashCode` 实现中的状态必须是 `equals` 状态的子集。在 `hashCode` 中使用合适的子集能够更快更好地生成哈希值。看起来像下面这样:


```java
private static final Equivalence<Point> EQ =
   Equivalence.forClass(Point.class)
       .andThen(Point::x)
       .andThen(Point::y, Equivalence.using(Objects::equals, x -> 0));
```


可以考虑为这种用法增加 API 支持,例如,`Equivalence.forClass(Point.class).andThen(Point::x).butNotHashing(Point::y)`,但没有必要支持到这种程度。这种用法并不常见,而且 hash 函数的最佳实践已经可以避免细小的碰撞。即使不增加 API 也已经可以实现。


是否应该支持自定义 hash reduce 函数?


传统的 `hashCode()` 实现会采用 `(x, y) -> 31 * x + y` 组合每个状态。通常这是一种不错的选择,目前没有看到令人信服的定制理由。无论采用哪种实现方式,都绝不应当给 hash reduce 函数指定默认实现,准备在将来对其改进。


(一种较为激进的方法是每次 JVM 调用都可以指定 hash 种子,以便进行测试。最近几年,Google 已经在我们的测试中对 hash 迭代顺序进行随机化并且取得了不错的效果)


在 equals 中使用 instanceof 还是 getClass()?


实现 `equals` 时,可以选择 `instanceof` 或者 `getClass()`,也可以交由实现者决定。这里不讨论哪种实现更正确。


幸运的是,有一种简单的方法有助于选择。`instanceof` 作为默认值会更灵活,因为这样用户可以在 `Equivalence` 链式检查中调用 `getClass()`,或者作为 `Equivalence.equals` 调用前的守护条件,例如:


```java
this.getClass() == o.getClass() && EQ.equivalent(this, o);
```


反过来用 `getClass()` 无法做到这点。


如何处理 null?


为了确保对称性,实现 `Object.equals()` 时,`equivalent(nonNull, null)` 必须安全地返回 `false`。`equivalent(null, null)` 应该返回 `true` 而不是抛出异常,这样可以尽可能确保一致性,不出现意料之外的结果。


与 Comparator 的关系?


Comparator 和 Equivalence 有一些明显的相似之处:都支持从对象实例中提取状态,分别用于 `compareTo` 和 `equals/hashCode`。


还有一些原因可以解释为什么必须把它们作为完全独立的 API 处理,而不是作为泛化(generalization)处理。


`Comparator` 可以通过 `x.compareTo(y) == 0` 实现 `Equivalence` 中的部分等价功能,但不能实现 `hashCode`。如果让 `Comparator` 继承 `Equivalence`,在调用 `hashCode` 时将抛出 `UnsupportedOperationException`。


也可以让 `Equivalence` 实现 `Comparator`,可以在比较函数里测试相关的状态。然而,这里的问题在于 `Equivalence` 中的比较函数会与 `Comparator` 功能重叠,而且想要比较的内容也许只是 `equals` 与 `hashCode` 中状态的子集。


第三种办法,同时创建 `Equivalence`、`Comparator` 以及一个状态列表,需要一个公用父类。这样不但增加了代码的复杂度,而且很可能对概念产生混淆。


设计相关问题


API 应该如何命名?


目前的两个备选方案:


  1. `Equalator`:参考 `Comparator`;

  2. `Equivalence`:类型的实例是等价关系。


我们的观点是,数学中有已经有了一个众所周知的定义,没必要再造一个新词。


数组应该比较内容相等还是引用相等?


”注意:“这个问题实际上讨论的是默认实现。由于 `equals` 和 `hashCode` 可以根据具体字段定制实现,因此可以自由选择。


这里至少有两派意见,本文只提供选项并不打算解决争论。


在数组上调用 `Object.{equals,hashCode}` 实际上是一个 bug,因此增加一个参数检查数组并自动调用 `Arrays.{deepEquals,deepHashCode}` 能够帮助用户避免 bug(通过静态分析检查避免在数组上调用 `Object.{hashCode,equals}` 是用户期待的结果)。


反对者认为,这么做会让数组使用更复杂。无论如何,使用者需要了解数组应当判断引用相等。如果只在这一个地方帮助用户,那么可能会顾此失彼,给他们带来麻烦。值得注意的是,Kotlin [采用了这种方法][2]。


[2]:https://blog.jetbrains.com/kotlin/2015/09/feedback-request-limitations-on-data-classes/


自定义比较


`Equivalence` 是否应当避免“装箱和可变参数”开销?例如,提供像 `IntEquivalence` 这样专门的接口,重载 `andThenInt(IntFunction)` 和 `andThenInt(IntFunction, IntEquivalence)`  builder 方法。


在某些情况下,这么做能够达到预期的性能。另一方面,又大大增加了 API 的复杂性。


既不增加 API 复杂性,又能满足性能要求,一种可能的方法是考虑转换策略:


“equivalent(T, Object) 或者 equivalent(T, T)”


有两种函数实现 `Equivalence` 等价:`equivalent(T, Object)` 和 `equivalent(T, T)`


使用 `equivalent(T, Object)` 可以在实现 `Object#equals` 时减少模板代码。我们希望更多地使用 `Equivalence` 实现而非 `Comparators`,后者只针对特殊场合适用(配合[concise 方法][3]实现会变得更简单)。


[3]:https://openjdk.java.net/jeps/8209434


```java
public boolean equals(Object other) {
 return EQ.equivalent(this, other);
}
```


或者:


```java
public boolean equals(Object other) = EQ::equivalent;
```


`equivalent(T, T)` 的优点,除 `Object#equals` 以外的方法都更简洁,提供额外的类型安全检查。同时,由于类型检查与使用独立,还避免了在 `getClass()` 与 `instanceof` 之间进行选择。


```java
public boolean equals(Object other) {
 return other instanceof Foo that && EQ.equivalent(this, that);
}
```


或者:


```java
public boolean equals(Object other) ->
   other instanceof Foo that && EQ.equivalent(this, that);
```


另一种选择是使用 `equivalent(T, T)`,在实现 `Object.equals` 前转换为 `Equivalence<Object>` 避免强制转化(尴尬的地方在于,这样牺牲了额外的类型安全检查)。


附录


示例实现


下面的代码只作阐明想法使用:


```java
interface Equivalence<T> {

boolean equivalent(T self, Object other);

int hash(T self);

static <T> Equivalence<T> of(Class<T> clazz, Function<T, ?>... decomposers) {
  return new Equivalence<T>() {
    public boolean equivalent(T self, Object other) {
      if (!clazz.isInstance(other)) {
        return false;
      }
      T that = clazz.cast(other);
      return Arrays.stream(decomposers)
          .allMatch(d -> Objects.equals(d.apply(self), d.apply(that)));
    }

    public int hash(T self) {
      return Arrays.stream(decomposers)
          .map(d -> Objects.hashCode(d.apply(self)))
          .reduce(17, (x, y) -> 31 * x + y);
    }
  };
}
}
```


equals 和 hashCode 实现中的 bug


我们在 `equals` 和 `hashCode` 方法的实现中发现了许多 bug,通常可以通过静态代码分析找到这些问题。


其中一些 bug 事后看来是显而易见的,不大可能发生在有经验的 Java 开发者身上,但它们的确出现了。一个原因可能是 `equals` 和 `hashCode` 通常被当作模板文件,因而对它们的检查不够仔细。随着时间推移,类不断修改 bug 会随之出现。


  • 重写 `Object.equals()`,但没有重写 `hashCode()`(`Object.hashCode` 要求,如果两个对象相等,那么两个对象中任意一个对象调用 `hashCode()` 必须产生相同的结果,只重写 `equals()` 显然无法做到这点)

  • `equals` 实现无限递归(应该有意识地使用 `==` 而非 `this.equals(other).`)

  • 比较字段或 getter 方法时没有配对,例如 `a == that.a && b == that.a`

  • 传入 `null` 作为参数时 `equals` 抛出 `NullPointerException`(应该返回 false)

  • 传入错误类型的参数时, `equals` 抛出 `ClassCastException`(应该返回 false)

  • 实现 `equals` 方法时调用了 `hashCode()`(频繁产生哈希冲突,导致误报)

  • `hashCode()` 包含没有在 `equals()` 方法中测试的状态(对象相等 hashCode() 必须相同)

  • `equals` 和 `hashCode` 实现,对数组成员比较引用相等或 hashCode 相等(用户可能希望比较的是值和 hashCode 相等)

  • 其他 bug:使用错误,例如比较两种不同的类型;或者定义错误,例如重写 `equals` 改变了默认实现,破坏了可替代性



标签:解决办法,Object,equivalent,Equivalence,equals,hashCode,other
来源: https://blog.51cto.com/u_15127686/2832727

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有