【问题标题】:Best practice to choose fields for equals() implementation为 equals() 实现选择字段的最佳实践
【发布时间】:2012-03-16 14:31:25
【问题描述】:

在编写单元测试时,我经常遇到equals() 测试中的某些对象(在assertEquals 中)的工作方式与实际环境中的工作方式不同的情况。以某个接口ReportConfig为例。它有id 和其他几个字段。从逻辑上讲,当它们的ids 匹配时,一个配置等于另一个配置。但是当涉及到测试一些特定的实现时,比如XmlReportConfig,显然我想匹配 all 字段。一种解决方案是不在测试中使用equals,而只是遍历对象属性或字段并进行比较,但这似乎不是一个好的解决方案。

因此,除了这种特定类型的情况之外,我想从语义上而不是技术上整理出实现等于的最佳实践。

【问题讨论】:

  • When writing unit-tests, I often face the situation when equals() for some object in tests -- in assertEquals -- should work differently from how it works in actual environment. 你为什么想要这个?
  • 因为有身份,有平等。在很多情况下,身份就足够了,尤其是在数据库应用程序中。但是,在测试中,很容易出现以下情况:您可能会克隆“之前”副本以与“之后”副本进行比较,在这些情况下,身份还不够,您希望更深入地了解对象的实际字段。

标签: java equals equality


【解决方案1】:

在语义上而非技术上实现 equals 的最佳做法是什么。

在 Java 中,equals 方法确实应该被视为"identity equals",因为它如何与Collection 和Map 实现集成。考虑以下几点:

 public class Foo() {
    int id;
    String stuff;
 }

 Foo foo1 = new Foo(10, "stuff"); 
 fooSet.add(foo1);
 ...
 Foo foo2 = new Foo(10, "other stuff"); 
 fooSet.add(foo2);

如果Foo 身份是id 字段,那么第二个fooSet.add(...) 应该不向Set 添加另一个元素,但应该返回false,因为foo1 和@987654336 @拥有相同的id。如果您定义 Foo.equals(和 hashCode)方法以包含 both id 和 stuff 字段,那么这可能会被破坏,因为 Set 可能包含对对象的 2 个引用,其中相同的 id 字段。

如果您没有将对象存储在Collection(或Map)中,那么您不必以这种方式定义equals 方法,但是许多人认为它是错误的形式。如果将来您确实将其存储在 Collection 中,那么事情将被打破。

如果我需要测试所有字段的相等性,我倾向于编写另一种方法。 equalsAllFields(Object obj) 之类的东西。

然后你会做这样的事情:

assertTrue(obj1.equalsAllFields(obj2));

此外,正确的做法是不定义考虑可变字段的equals 方法。当我们开始讨论类层次结构时,这个问题也变得很困难。如果子对象将equals 定义为其本地字段和基类equals 的组合,则其对称性已被破坏:

 Point p = new Point(1, 2);
 // ColoredPoint extends Point
 ColoredPoint c = new ColoredPoint(1, 2, Color.RED);
 // this is true because both points are at the location 1, 2
 assertTrue(p.equals(c));
 // however, this would return false because the Point p does not have a color
 assertFalse(c.equals(p));

我强烈推荐的更多阅读内容是这个很棒的页面中的“陷阱#3:根据可变字段定义等于”部分:

How to Write an Equality Method in Java

一些额外的链接:

哦,为了后代,无论您选择比较哪些字段来确定相等性,您都需要在 hashCode 计算中使用相同的字段。 equals 和 hashCode 必须是对称的。如果两个对象相等,则它们必须具有相同的哈希码。反之亦然。

【讨论】:

  • “如果您不将对象存储在 Collection 中,那么您不必根据身份定义 equals 方法” OO 的全部意义在于您的类代表一个独立的抽象。你会泄露需求并为特定目的调整你的类接口,这会使它变得毫无意义。
  • 尽管@Jubbat 在学术上是正确的,但 Java Collections 几乎影响了 JDK 的每个部分。你可以忽略他们成为一个 OO 纯粹主义者,但这样做是有风险的。
  • 仍然认为最好在类文档中表示不变量(如唯一标识符)并为它们使用断言(请参阅下面的答案)
  • 没有。 HashSet 的问题是 equals 在您的示例中被 not 调用。如果调用了equals,那么它将返回true,因为foo1.equals(foo1) == true。因此,不会将第二个引用添加到集合中。
  • FYI... 关于“如果 Foo 身份是 id 字段,那么第二个 fooSet.add 不应该向 Set 添加另一个元素,而是应该替换第一个元素,因为它们具有相同的 id。”,@ 987654325@ 合约承诺相反:如果该集合已经包含该元素,则调用保持该集合不变并返回 false。
【解决方案2】:

复制自Object.equals(Object obj)javadoc:

指示其他对象是否“等于”这个对象。

equals 方法在非空对象引用上实现等价关系:

  • 它是自反的:对于任何非空引用值 x,x.equals(x) 应该返回 true。
  • 它是对称的:对于任何非空引用值 x 和 y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 才应该返回 true。
  • 它是可传递的:对于任何非空引用值 x、y 和 z,如果 x.equals(y) 返回 true 并且 y.equals(z) 返回 true,则 x.equals(z) 应该返回 true .
  • 这是一致的:对于任何非空引用值 x 和 y,x.equals(y) 的多次调用始终返回 true 或始终返回 false,前提是没有修改对象上 equals 比较中使用的信息。李>
  • 对于任何非空引用值 x,x.equals(null) 应该返回 false。

这对我来说很清楚,这就是 equals 应该如何工作。至于选择哪些字段,您可以选择所需的任何字段组合来确定某个其他对象是否“等于”这个对象。

对于您的具体情况,如果您在测试中需要更广泛的平等范围,那么您可以在测试中实现它。您不应该仅仅为了使其适合而修改您的 equals 方法。

【讨论】:

  • 同意。另外,我在自己的回答中添加了常识的“伪”规则。
【解决方案3】:

你应该在equals中使用所有重要的变量,即不是从其他变量派生的变量。

这是来自 Effective Java:

对于类中的每个“重要”字段,检查参数的该字段是否与该对象的相应字段匹配。

如果您想匹配 id,因为它是该类的唯一标识符,那么只需比较 id 值,在这种情况下不要使用 equals。

如果您有唯一标识符,则该语言不允许您强制没有其他对象具有该标识符或其余变量的值匹配。但是,您可以在类的文档中定义它,并且可以在 equals 实现或其他地方使用断言,因为它是您的类语义给出的不变量。

【讨论】:

  • "如果您想匹配 id,因为它是该类的唯一标识符,那么只需比较 id 值,在这种情况下不要使用 equals。" -- 将实例放入Set 或Map 怎么样?为此,我需要正确的等号,并且通过 id 比较它们。这种情况怎么办?
  • @AndreyBalaguta:如果 id 确实应该是唯一的,则不需要覆盖 Equals。相反,使用某种Map<Integer,YourType> 来保存对任何给定ID 将存在的唯一YourType 的引用。
【解决方案4】:

我不会在写equals() 时考虑单元测试,两者都是不同的。

您可以通过实现equals() 和hashcode() 用一个或一组属性定义每个对象的相等性。

在你的测试中如果你想比较对象的所有属性,那么显然你需要调用每个方法。

我觉得分开处理比较好。

【讨论】:

    【解决方案5】:

    我认为覆盖equals() 方法的唯一最佳实践是常识。

    除了 Java API 的 equivalence definition 之外,没有其他规则。一旦您选择了相等的定义,您还必须将它应用到您的 hashCode() 方法中。

    我的意思是,作为一名开发人员,您、您的同事和维护人员应该知道您的实例(比如说Watermelon)何时等于另一个 Object 实例。

    【讨论】:

      猜你喜欢
      • 2015-11-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多