为什么在Object类中定义了equals和hashCode方法?

17
决定在java.lang.Object中包含这些方法的原因是什么?对于许多类来说,相等性和哈希值并没有意义。更合理的做法是创建两个接口:
interface Equalable {
    boolean equals(Equalable other);
}

interface Hashable extends Equalable {
    int hashCode();
}

例如,HashSet的定义可能如下所示:
class HashSet<T extends Hashable> ...

它可以防止一些初学者常犯的错误——在没有实现equals/hashCode的情况下使用项目集合。

点击这里阅读Jon Skeet关于相关主题的好文章。 - Jordão
2
链接似乎已经失效。这篇文章可能是同一篇。 - tkokasih
1
这是一个错误。 - Matt Timmermans
10个回答

15
当我们实现一个接口时,我们注入(或接受)由接口定义的合同。
Equalable和Hashable是两个不同的合同。但如果我们仔细看,我们会发现它们彼此依赖,这意味着它们是单个接口的一部分,类似于EqualableAndHashable。
现在显而易见的问题是,它们应该成为这个新EqualableAndHashable接口或Object的一部分?
让我们找出答案。我们有==(等于运算符)来检查两个对象的相等性。==运算符确认两个不同的原始值/对象的值/引用是否相等。但只通过使用==运算符来回答这个问题并不总是可能的。
现在的问题是,这种相等性,也是一个合同,是否应通过接口注入或作为Object类的一部分?
如果我们仔细看,我们不能仅仅说:
TypeX不保证平等合同。
如果某些对象类型提供平等,而有些则不提供,则会导致混乱。这意味着TypeX对象必须尊重平等合同,这也适用于所有其他对象类型。因此,它不应从接口注入平等性,因为平等性应默认包含在任何对象的合同中,否则将会创建混乱。
因此,我们需要让对象提供equals方法的实现。但它不能仅实现equals方法,它还需要实现hashcode方法。

2
“=” 实际上是赋值运算符 ;) - Dave Newton
3
我不理解你提到的“混乱”。我一直在使用这些方法将我的类放入集合和映射中。对我来说,避免默认实现可能更简单,因此我支持从Object中删除这些方法。 :D - The Impaler

2

java.lang.Object中的默认实现是有意义的。通常情况下,它已经足够好了。在JPA / web应用程序中,我很少甚至从未覆盖equals和hashCode。

一个更好的问题可能是:对于不可变值对象(例如String、Long等),为什么不能像C#中那样重写==运算符以调用equals()?我看到的错误比默认的equals/hashCode没有做正确的事情要多得多。例如,

Long x = obj.getId(); 
Long y = obj2.getId();  
if (x == y) { // oops, probably meant x.equals(y)! }

这是一个公平的问题,为什么默认方法没有像默认的Object.clone()一样被锁定在标记接口后面呢?虽然有一个默认实现,但你必须明确地承认你想要使用它,通过实现Cloneable接口。同样可以有类似的标记接口,如Collectible或Equatable,然后集合方法的签名可以是Equatable而不是Object。


1

在@Kowser的答案中添加有关'chaos'的注释:

等式的隐含逻辑要求它必须满足以下特征:

  • 它是自反的:对于任何非空引用值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。
  • ...

显然,如果Object中未定义equals方法,则会破坏“对称”和“传递”,这相当于破坏了等式的内在逻辑。


1

就我个人而言,如果它们在一个接口中,我会把它们都放在那里,以避免至少一类equals/hashCode错误。

我认为你会想要在Object中实现一个回退机制,这意味着无论是否有接口,所有东西都将有一个实现。

我怀疑很多都是历史原因;今天编写Java程序看起来与过去相比有很大的不同。


1

嗯,不确定,但当Java 1.0发布时,泛型还不存在。它们是在2004年的Java 5.0中添加的...因此,您的建议无法在Java 1.0中实现。


2
如果TreeSet可以要求键实现Comparable,如果没有指定Comparator,那么为什么HashSet不能要求Hashable呢?这不仅涉及到泛型。 - meriton

0

实际上,这只是为了方便起见,这样做更好。好吧,如果没有.equals方法,想想要实现对象相等需要做些什么:

AreEqual(Object obj1,Object obj2) {
  if(!obj1 instanceof Equalable) return false;
  if(!obj2 instanceof Equalable) return false;
  return ((Equalable)(obj1).equals((Equalable)obj2);
}

即使是hashCode也是如此。有时候,引用相等已经足够了。如果你让HashSet只接受实现了Hashable的对象,那么即使你只想要引用相等,你也必须显式地将你的类变成Hashable。这样会降低一个本来很好的数据结构的通用性。

最好让Object具有默认(有时足够)的.equals和.hashCode函数,并为新手造成一些问题,而不是让语言的经常使用者通过更多的繁文缛节。


0

任何对象,无论其类型如何,都可以明智地回答它是否等同于任何其他对象,即使另一个对象的类型是它从未听说过的。如果它从未听说过另一个对象的类型,仅此事实就足以让它报告它与后者对象不等同。


0

这是一个通用实现。如果需要,您应该覆盖实现。否则,您将拥有一个合理的默认实现。

至少equals是必须的。为基本操作创建接口可能会涉及太多开销。


0
如果您有一个对象列表并调用contains方法,Java应该做什么?我认为默认实现(比较引用)是一个不错的决定。这样,您就不必为集合中使用的每个类自己实现equalshashcode

0

最初,在Java中没有泛型。为了解决这个问题,允许任何Object成为任何集合的成员,因此任何Object都需要hashCodeequals。现在已经太深入人心,无法改变。


1
泛型的缺失是无关紧要的。例如,如果没有指定比较器,它并不会阻止TreeSet需要键实现Comparable接口。 - meriton

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