可变集合是否应该重写equals和hashCode方法?

25

我在想是否重写可变集合的equalshashCode方法是个好主意。这将意味着如果我将这样的集合插入到一个HashSet中并修改了集合,HashSet将不能再找到它。这是否意味着只有不可变集合应该重写equalshashCode方法,或者这只是Java程序员必须应付的麻烦?


5
在面向对象(OO)层次结构的顶部存在equalshashCode,这是Java程序员不得不应付的麻烦。equalshashCode本质上是有缺陷的,但原因不仅限于你提到的那个。我主要在Java中使用“不可变对象上的OO”,即使这样做,equalshashCode也是有问题的。在大多数情况下,拥有这些方法是没有意义的:对于任何非final类,无法满足equalshashCode的规范。这在《Effective Java》中有很好的解释。 - SyntaxT3rr0r
1
请查看我在这里的相关问题,有10个赞和几个收藏:https://dev59.com/V3E95IYBdhLWcg3wrP6w 对于除了只将一种类型的不可变对象(没有继承,没有其他)放入集合的最简单情况之外的任何情况,equalshashCode都是真正糟糕的。你的情况并不简单,会有很多问题。OO层次结构顶部的equalshashCode是一个错误,纯粹而简单(但大多数Java程序员没有意识到)。 - SyntaxT3rr0r
2
@SyntaxT3rr0r - 在继承链中有处理这个问题的方法,请参见http://www.angelikalanger.com/Articles/JavaSolutions/SecretsOfEquals/Equals.html。此外,实体对象通常是可变的,但具有一些不可变的“id”,用于这些方法。因此,它们并非“在所有java中根本存在问题”。像所有工具一样,您需要知道如何使用它们。 - jtahlborn
StringBuffer类和StringBuilder类也不覆盖equals()和hashCode()方法,因为它们是可变的。 - Akhilesh Dhar Dubey
8个回答

5

如果你的类应该像值类型一样运作,那么你需要重写equalshashCode方法。但是对于集合来说,通常情况下并不需要这么做。

(我并没有太多Java经验,这个答案是基于C#的。)


5

深度和浅度相等的问题比Java更为复杂;所有面向对象语言都必须关注此问题。

您添加到集合中的对象应该覆盖equals和hashCode方法,但默认行为内置于集合接口的抽象实现中足以满足集合本身的要求。


4
@duffymo: +1... 但是所有的面向对象语言都没有犯Java创造者所犯的同样错误:将equalshashCode放在面向对象层次结构的顶部,好像这有任何意义(实际上并没有)。应该有一个Equalable接口或类似的东西。不,我不是Java的憎恨者,而是Java的粉丝 :) - SyntaxT3rr0r
我不认为这是一个错误。所有对象都需要身份标识;这就是为什么它在java.lang.Object中。"Equalable"?我认为那是一个错误,但这只是我的看法。8) - duffymo
1
@duffymo:但是身份验证与“equals”和“hashCode”的概念毫无关系。它只是与面向对象不兼容,就是这样。有一篇关于Java equals/hashCode SNAFU的绝佳文章(Bill Venners和Scala的创建者之间的对话),再次在《Effective Java》中解释了这一点。对于任何非final类,它都是错误的事实。对于可变对象,这个概念也是错误的事实。在某一点上,你必须想知道它们在OO层次结构的顶部是否仍然有任何意义。 - SyntaxT3rr0r
足够了,SyntaxT3rr0r,别再喝五小时能量了。放松点。编写等号和哈希码与标识符相符是可行的,这是Hibernate和持久化对象的推荐最佳实践。你的观点很好,但我在浏览器中看到的传递方式有些过于激烈了。 - duffymo
@语法:我在哪里可以找到那篇“好文章”?听起来很有趣。 - fredoverflow
显示剩余3条评论

2

对于任何可变类,情况都是一样的。当您将一个实例插入到HashSet中并调用一个变异方法时,您会遇到麻烦。所以,我的答案是:如果有用途,就可以使用。

当然,在将其添加到HashSet之前,您可以使用不可变包装器来包装您的集合。


2

我认为更重要的问题是,如果有人试图将FredCollection的一个实例添加到Set两次,应该发生什么。

FredCollection c = ...
set.add(c);
set.add(c);

在此之后,setsize() 应该是 2 还是 1

你是否会需要测试两个不同的 FredCollection 实例的“相等性”?我认为这个问题的答案比其他任何事情都更重要,以确定你的 equals()/hashcode() 行为。


我们能否请您提供一个工作示例的插图,以便方便日后参考。 - Deepak
size() 将会是 1,因为通常情况下 Set 只包含每个对象的一个。c 没有被分配新值。即使您调用 c.add('cow'),它仍将是同一个对象 c。默认的 equals() 方法是一个浅比较的 ==。 - Chloe

1

这不仅仅是在集合中的问题,也是在可变对象中普遍存在的问题(另一个例子:Point2D)。而且,是Java程序员最终需要考虑到的潜在问题。


1

你不应该覆盖equals和hashCode方法,以便它们反映可变成员。

这更多是我的个人观点。我认为哈希码和equals是技术术语,不应用于实现业务逻辑。想象一下:你有两个对象(不仅仅是集合),并询问它们是否相等,那么有两种不同的答案:

  • 技术性:如果它们表示相同的对象,则它们相等,这与是否为相同对象(如果您考虑代理、序列化、远程内容等)有所不同
  • 业务逻辑:如果它们看起来相同(具有相同属性),则它们相等 - 这里重要的事情是,即使在一个应用程序中,即使对于同一类,也没有唯一的相等定义。 (示例问题:什么时候两块石头是相等的?)

但是,由于equals被技术性的东西(如HashMap)使用,因此应以技术性的方式实现它,并通过其他方式(例如比较器接口)构建与业务逻辑相关的equals。 对于您的集合,这意味着:不要覆盖equals和hashCode(以违反技术合同的方式)。

注意:如果将可变对象用作映射键,必须非常小心。如果对象的值以会影响equals比较的方式改变,则映射的行为未指定。 (Map的 Java 文档)。

对于错误引用Java API的人,应该进行负面评价,因为这可能会严重困扰阅读此内容的人。实际上,完整的引用是(强调我的):“只要在Java应用程序的执行期间多次在同一对象上调用它,hashCode方法必须始终返回相同的整数,前提是没有修改用于对象上的equals比较的任何信息。”因此,只要可变成员也在equals中进行了比较,就可以在hashCode中使用它。是否实现可变对象的equals和hashCode是另一回事,这取决于实际情况。 - Medo42
取消了踩,谢谢。我认为在业务逻辑中使用equals没有问题,只要你意识到其影响-有时候你想知道例如一组人是否包含某个人,即使你没有确切的对象(可能已经从数据库中读取)。也许问题在于“equals”暗示着检查相等值(我们称之为“业务相等性”),而实际上的想法是检查两个对象是否表示同一件事情(“业务身份”)。 - Medo42

0

equalshashCode的一个基本困难是,有两种逻辑方式可以定义等价关系;一些类的使用者将希望有一种定义,而该类的其他使用者将希望另一种定义。

我将定义这两个等价关系如下:

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

  • 如果在一个程序中,该程序没有持久化与标识相关的哈希函数返回的值,那么交换所有对X的引用和所有对Y的引用将不会改变程序状态,则两个对象引用X和Y具有等效状态。

请注意,第二个定义主要与两个东西持有某些可变类型(例如数组)的对象的常见情况相关,但可以确定,在感兴趣的特定时间范围内,这些对象不会暴露给可能会改变它们的任何东西。在这种情况下,如果“持有者”对象在所有其他方面都是等价的,则它们的等价性应取决于它们所持有的对象是否符合上述“第二”等价性定义。

请注意,第二个定义不涉及对象状态如何改变的任何细节。此外,请注意,对于等价关系的任何一个定义,不可变对象都可以将具有相同内容的不同对象报告为相等或不相等(如果X和Y之间唯一的区别是X.Equals(X)报告为true而X.Equals(Y)报告为false,那将是一个差异,但最有用的可能是让这些对象使用引用标识符进行第一个等价关系和其他方面的等价关系进行第二个等价关系。

不幸的是,由于Java只提供了一对定义等价关系的类,类设计者必须猜测哪种等价关系的定义对类的消费者最相关。虽然有很多理由支持始终使用第一个等价关系,但第二个等价关系通常更实用。第二个等价关系的最大问题是,类无法知道使用该类的代码何时需要第一个等价关系。


-1

equals 用于向集合中添加/删除元素,例如 CopyOnWriteArraySet、HashSet 等,如果两个不同对象的 hashCode 相等,则它们可以使用 equals 进行操作。equals 需要对称,即如果 B.equals(C) 返回 true,则 C.equals(B) 应该返回相同的结果。否则,在这些 XXXSets 上进行添加/删除操作会表现出令人困惑的行为。请查看 Overriding equals for CopyOnWriteArraySet.add and remove,了解如何不正确地重写 equals 如何影响集合上的添加/删除操作。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接