【问题标题】:Java: clean way to automatically throw UnsupportedOperationException when calling hashCode() and equals()?Java:调用 hashCode() 和 equals() 时自动抛出 UnsupportedOperationException 的干净方法?
【发布时间】:2010-02-05 07:11:26
【问题描述】:

我们有一个 OO 代码库,在很多情况下 hashcode()equals() 根本不起作用,主要是因为以下原因:

没有办法扩展 可实例化的类并添加一个值 组件,同时保留等式 合同,除非你愿意 放弃面向对象的好处 抽象。

这是 Joshua Bloch 的“Effective Java”中的一段引述,关于该主题的更多信息请参见此处的一篇精彩的 Artima 文章:

http://www.artima.com/lejava/articles/equality.html

我们对此非常满意,这不是这个问题的目的。

问题是:看到在某些情况下您无法满足equals() 合同的事实,那么自动使hashcode()equals() 抛出UnsupportedOperationException 的干净方法是什么?

注释会起作用吗?我正在考虑类似@NotNull 之类的事情:每个@NotNull 合同违规都会自动引发异常,除了用@NotNull 注释您的参数/返回值之外,您无事可做。

很方便,因为它是 8 个字符(“@NotNull”),而不是不断重复相同的验证/抛出异常代码。

在我担心的情况下,在 hashCode()/equals() 毫无意义的每个实现中,我们总是重复同样的事情:

@Override
public int hashCode() {
    throw new UnsupportedOperationException( "contract violation: calling hashCode() on such an object makes no sense" );
}

@Override
public boolean equals( Object o ) {
    throw new UnsupportedOperationException( "contract violation: calling equals() on such an object makes no sense" );
}

但是这很容易出错:我们可能会错误地忘记剪切/粘贴它,这可能会导致用户误用此类对象(例如尝试将它们放入默认 Java 集合中)。

或者如果不能通过注释来创建这种行为,AOP 会起作用吗?

有趣的是,真正的问题是hashCode()equals() 出现在Java 层次结构的顶部,这在某些情况下根本没有意义。但是我们如何干净利落地处理这个问题呢?

【问题讨论】:

  • +1 用于在不需要的时候拒绝实现 hashCode 和 equals,甚至确保它们不能通过抛出异常来调用。这是对您经常听到的口头禅的一个可喜的变化,即您必须做的第一件事是实现这两种方法(并花很多心思在它们上以使它们正常工作),即使大多数对象从不需要任何一种方法。跨度>
  • 当您编写实现接口的新类时,Eclipse 为您提供的自动生成方法的一个相关问题是它们都生成到 return nullreturn false、@987654336 @。我希望默认为throw UnsupportedOperationException("TODO")
  • @Thilo 我用我的 Eclipse 模板就是这样做的,所有生成的方法体都会抛出 UnsupportedOperationException
  • @Bemrose:我不同意。如果您想检查它们是否是相同的参考,请使用 ==(尽管我并不经常需要这样做,但确实没有害处),而不是 equals(),这将是非常有害的。因为你无法知道 equals() 方法是否被覆盖,所以依赖 Object 的默认 equals() 方法使用 == 而不是自己直接使用 == 是非常有害的。
  • @Thilo:我已经更改了我的 Eclipse 模板来做到这一点。它还添加了当前日期,因此我可以知道方法未实现多长时间。

标签: java equals design-by-contract


【解决方案1】:

我同意您对 hashCodeequals 首先在 Object 中定义的问题的评估。长期以来,我一直认为应该以与排序相同的方式处理相等性 - 一个界面说“我可以与 X 的实例进行比较”,另一个说“我可以比较 X 的两个实例”。

另一方面,这实际上是否对您造成了任何错误?人们是否一直在尝试在不应使用的地方使用 equalshashCode?因为即使您可以使 您的 代码库中的每个类在这些方法被不当调用时都抛出异常,但对于您正在使用的其他类(无论是来自 JDK 还是第三方)来说,这也不是真的图书馆。

我相信您可以使用某种形式的 AOP 来做到这一点,无论是正常的注释处理还是其他方式 - 但您是否有证据表明奖励值得付出努力?

另一种看待它的方式:这在您扩展另一个已经覆盖hashCodeequals 的类的情况下,对吗?否则,您可以使用 Object 的 hashCode/equals 方法的“平等 = 身份”性质,这仍然很有用。你有很多属于这一类的课程吗?你能不能只写一个单元测试来通过反射找到所有这些类型,并在你调用hashCode/equals时检查这些类型是否抛出异常? (如果他们有一个无参数的构造函数,这可以是自动化的,或者有一个已经检查过的类型的手动列表 - 如果有一个不在“已知良好”列表中的新类型,单元测试可能会失败。)

【讨论】:

  • +1 表示想要一个单独的 equals/hashCode 接口。这将阻止人们将对象用作在其中不起作用的哈希键(在这方面,不可变接口也很好)。
  • 嗨,乔恩,关于奖励/努力,这当然是一个问题:但如果在注释出现时,我建议使用 @NotNull 注释,可能也会这样说。只需要有人写一次,然后我们都可以重用它。有趣的是,我正在使用自定义模板,并没有考虑简单地默认情况下让 hashCode() 和 equals() 抛出 UnsupportedOperationException,然后简单地更改 equals() 和 hashCode() 有意义的类的实现。跨度>
  • 我认为可空性往往是一个更大的问题。由于 nullity 问题,我看到了很多错误,但我不记得曾经尝试过对不应该散列的东西进行散列。
【解决方案2】:

我不明白你为什么认为“在某些情况下你不能满足 equals() 合约”?相等的含义由类定义。因此,使用 Object 的 equal 是完全有效的。如果您没有覆盖 equals,那么您将每个实例定义为唯一的。

似乎有一种误解,认为 equals 是那些总是需要覆盖的方法之一,并且它必须检查其所有字段。我的观点正好相反——除非你对相等的定义不同,否则不要覆盖 equals。

我也不同意 artima 的文章,尤其是“陷阱 #3:根据可变字段定义 equals”。类根据可变字段定义其相等性是完全有效的。用户在将它与集合一起使用时要注意这一点。如果一个可变对象在其可变状态上定义了它的相等性,那么不要期望两个实例在一个实例发生变化后相等。

我认为抛出 UnsupportedOperation 违反了 equals 的冲刺。对象的等于状态:

Object类的equals方法 实施最有区别的 可能的等价关系 物体;也就是说,对于任何非空 参考值 x 和 y,此方法 当且仅当 x 和 y 时返回 true 引用同一个对象(x == y 有 值 true)。

所以,我应该能够调用 equals 并根据 Object 的 equals 定义或覆盖的 equals 定义来获取真或假值。

【讨论】:

  • +1:即使没有其他用处,您仍然可以通过简单地使用equals/hashCode 中的Object 实现来应用对象标识作为相等度量。
  • @Joachim:是的,但是如果对象相等对他的类没有意义,并且他根本不需要 equals/hashCode,那么抛出 UnsupportedOperation 更有意义。至少这有助于在代码的某些部分错误地调用这些方法时发现错误。
  • @Kuo:-1,我不这么认为,我知道,而且已经证明:) 你不同意 Martin Odersky(Scala 的作者)和 Joshua Bloch。美好的。你有权发表你的意见。已经证明,一开始你根本无法保证 equals() 的传递性契约。甚至来自 SO 名气和 c.l.j.p. 的全能 Jon Skeet。成名同意我的说法,即在对象层次结构的顶部存在 equals() 和 hashCode() 是这里的问题。我担心的正是 Thilo 评论的:在没有意义的地方,它应该抛出 UnsupportedOperationException。
  • @SyntaxT3rr0r:究竟什么可以“证明”?一个类显然有可能以破坏的方式定义 hashCodeequals(例如,让这些函数返回随机值),但这绝不会阻止正确设计的类实现 equals 以便与任何其他设计合理的类。至于它们是否属于Object,我假设如果XY可以共存于同一个集合中,应该可以问X是否相当于YX应该能够毫无困难地回答。
  • @SyntaxT3rr0r:equalshashCode 的一个更大问题源于 Java 没有区分用于封装身份的对象引用,那些仅封装身份以外的不可变属性的对象引用,封装非共享可变状态的封装,封装对可变类型实例的引用但不会将这些实例暴露给任何可能改变它们的事物的封装,以及封装可变状态和身份的封装。为了让两个系列能够明智地相互比较,他们必须知道......
【解决方案3】:

为什么不让您的 IDE(Eclipse/NetBeans/IntelliJ)为您生成 hashCode()equals() 方法。他们在这方面做得很好。

当然,AOP 会起作用,但它相当复杂。这意味着您将无法在几乎没有集合或实用程序的情况下使用这些对象。

另一个合乎逻辑的解决方案是只删除那些不起作用的方法的实现,从而有效地只留下 Object 中的实现。

【讨论】:

  • @Bozho:+1,这就是 Hemal Panda 在评论中所说的他正在做的事情,我没有考虑过(即使我们使用自定义模板也很困难)。我太专注于“@NotNull”之类的东西:)
【解决方案4】:

在Java或.NET中的所有对象之间至少可以定义两种等价关系:

  • 如果用对 Y 的引用覆盖 X 不会改变 X 或 Y 的任何成员的当前或未来行为,则两个对象引用 X 和 Y 完全等效。

  • 两个对象引用 X 和 Y 具有相同的状态,如果在一个没有持久化从身份相关哈希函数返回的值的程序中,将所有对 X 的引用与对 Y 的所有引用交换将使程序状态保持不变。

我有一个引用 (X) 指向 FordBlazer。我有另一个 (Y) 指向 SiameseCat。它们是等价的吗?不,他们不是,所以X.equals(Y) 应该是假的。对象的类型彼此没有关系这一事实不是问题——如果有的话,它会让事情变得更容易(如果唯一可以等同于 SiameseCat 的是另一个 SiameseCat,Y 的事实是't a SiameseCat 意味着 X.equals() 不需要检查其他任何内容。

虽然对于特定对象是否应该实现第一个或第二个等价定义可能存在一些争论,但值得注意的是,任何定义 equals 的对象都将不同的对象报告为不相等,而不管其状态的任何其他方面如何与自身一致(如果 X.Equals(X) 与 X.Equals(Y) 不匹配,则意味着 Y 的行为与 X 不同)。因此,如果对equals 没有任何更好的处理,那么从object 继承的默认定义是一个非常好的和有意义的定义。

hashCode 可能遇到问题的唯一情况是,当对象存储在 HashTable 中时,代码可能(不明智地)改变对象的某些方面。对此的适当补救措施是让hashCode 不依赖于对象状态的任何可变方面。如果一个对象的状态除了它的类之外没有有意义的不可变方面,只需为该类组成一个任意数字并让hashCode 总是返回它。大型哈希表对此类对象的性能较差,但小型哈希码可以正常工作。不能为一个类型定义一个好的哈希码这一事实不应该阻止它在一个包含十几个项目的 HashTable 中使用。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-07-28
    • 2023-03-29
    • 2020-06-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-06-02
    相关资源
    最近更新 更多