为什么Java的`equals()`方法默认不进行深层比较?

8
众所周知,如果对象的equals()方法没有被覆盖,则它是一种“浅比较”,相当于使用“==”运算符。(例如,请参阅https://docs.oracle.com/javase/tutorial/java/IandI/objectclass.html。)
问题:为什么Java默认不提供“深度比较”的equals()方法?也就是说,在每个实例变量上递归调用equals()方法。最终,递归将到达基元类型并停止。如果这个深度比较的equals方法成为默认值,是否有任何缺点?

性能是首要考虑的因素。你有测量过在JVM启动期间执行这个操作的影响吗? - efekctive
6
你认为通用实现的工作方式应该如何? - Thorbjørn Ravn Andersen
5
实现如何区分是对象逻辑值的一部分的引用和仅仅是关联的引用?实现如何处理包含循环引用图的引用?请注意,该定义是在语言加入注释之前创建的。 - Andy Thomas
你怎么能期望框架猜测你的对象由什么组成以进行深度比较呢?反射?这将非常低效。顺便说一下,Arrays有一个deepEquals方法,它知道如何进行深度比较。如果你想调用deepEquals,你可以重写equals并调用Objects.deepEquals(o1,o2),但是equals()也能完成同样的工作。 - SomeDude
3
关于实现,也许第一次尝试可以在编译时完成:编译器找到所有实例变量,并生成一个调用这些变量上的equals()方法的equals()方法。这将避免运行时反射的开销。 - flow2k
显示剩余2条评论
3个回答

12
这个深度比较作为默认值是否有任何缺点呢?
是的。这些包括:
  • 默认实现无法区分在对象的逻辑值中的引用和与其他对象的关联仅仅是关系而已的引用。例如,假设您有一个Person类,引用了一家公司。您有两个具有相同名称、社会安全号码、出生日期等信息的Person实例。其中一个引用一个旧公司。即使一个对象的关联过时,您可能希望这两个引用同一实际人物的Person实例相等。
  • 深度相等测试通常比当前默认值慢,可能显著慢。很多情况下这是不必要的。当前默认值确保除非有人明确指定否则相等测试始终快速。
  • 深度比较需要处理引用中的循环。这将需要一些方法来记住已经遍历过的对象。这将需要内存来跟踪这些对象,可能需要大量内存。相等测试可能导致OutOfMemoryError。
当前默认实现快速且不做任何假设。这是一个好的默认值。有时候,您需要根据自己的知识覆盖默认值,无论其根物理表示如何,都能够包含对象的逻辑值。

4

深度比较比两个引用的比较要复杂得多,而且需要更多的时间。这对于简单的对象可能还可以接受,但是当你有非常复杂的数据结构时(例如一个包含一万个元素的树),系统如何知道比较应该有多“深”?


6
任何用于树的相等性测试都会涵盖所有节点。无论默认实现还是临时实现,它都可能很慢。真正的问题是带有环的图形。带有指向父节点的指针的树实现将包含父节点和子节点之间的引用循环。 - Andy Thomas

0
对于大多数对象,引用相等是正确的实现方式。 "深度" 相等是为了维护状态的少数对象而设计的。不仅您的提议会遇到许多人在此处描述的问题,而且对于大多数类型来说也是错误的。

这是一个非常强烈的陈述。对象的逻辑值并不总是对应于其物理表示。而且默认实现甚至不测试引用字符串对象的相等性。 - Andy Thomas
2
我不同意这是大多数对象的“正确”实现方式:只需看看您必须在多少类中覆盖equals方法。仅仅查看JDK源代码,虽然不是大多数情况,但也绝非微不足道的数量。 - Andy Turner
该论断是“大多数类型”具有恰当等价关系的身份,而不是“所有类型”。并没有声称值对象是“无足轻重”的少数派。也没有提到任何关于物理表示的内容。你正在与一只纸老虎作斗争。 - Lew Bloch
你正在与一个稻草人争论。我们两个Andy都不同意你的说法,即引用相等对于“大多数类型”已经足够了。我们两个都没有暗示它对所有类型都不足够。 - Andy Thomas
@AndyTurner说:“...只需看看您必须在其中覆盖equals的类的数量。仅查看JDK源代码,它并不占多数,...” - Lew Bloch
天地间的课程比JDK源代码中存在的还要多,霍雷肖。 - Andy Thomas

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