Java:为什么集合会接受比较器而不接受(假设的)Hasher和Equator?


25

当您使用接口的不同实现时,此问题最明显,并且出于特定集合的目的,您仅关心对象的接口级视图。例如,假设您有一个像这样的接口:

public interface Person {
    int getId();
}

实现hashcode()equals()实现类的通常方法在equals方法中将具有如下代码:

if (getClass() != other.getClass()) {
    return false;
}

这会导致问题,当你混的实现PersonHashMap。如果HashMap仅关心的接口级视图Person,则最终可能会出现仅在实现类上有所不同的重复项。

您可以通过equals()对所有实现使用相同的Liberty 方法来使这种情况起作用,但是您冒着equals()在不同上下文中做错事的风险(例如,比较Person数据库记录支持的两个带有版本号的)。

我的直觉告诉我,应该对每个集合而不是对每个类定义相等性。当使用依赖于排序的集合时,可以使用自定义Comparator在每个上下文中选择正确的排序。基于散列的集合没有类似物。为什么是这样?

为了澄清起见,此问题与“ 为什么.compalsTo()在接口中而.equals()在Java类中? ”不同,因为它处理集合的实现。compareTo()equals()/ 和/ hashcode()都存在使用集合时的通用性问题:您不能为不同的集合选择不同的比较函数。因此,出于这个问题的目的,对象的继承层次结构根本无关紧要;重要的是比较函数是按对象定义还是按集合定义。


5
您可以随时引进包装对象为Person实现预期equalshashCode行为。然后,您将拥有一个HashMap<PersonWrapper, V>。这是一个纯OOP方法并不完美的示例:并非对对象的每个操作都适合作为该对象的方法。Java的整个Object类型是不同职责的集合体,只有getClassfinalizetoString方法似乎可以被当今的最佳实践证明是合理的。
阿蒙2015年

1
1)在C#中,您可以将传递IEqualityComparer<T>给基于哈希的集合。如果未指定,则使用基于Object.Equals和的默认实现Object.GetHashCode()。2)Equals在可变引用类型上覆盖IMO并不是一个好主意。这样,默认等式非常严格,但是当需要通过custom时,可以使用更宽松的等式规则IEqualityComparer<T>
CodesInChaos

Answers:


23

这种设计有时被称为“普遍平等”,它认为两个事物是否相等是普遍属性。

而且,相等性是两个对象的属性,但是在OO中,您总是在一个对象上调用一个方法,而该对象只能决定如何处理该方法调用。因此,在像Java这样的设计中,相等性是被比较的两个对象之一的属性,甚至不可能保证相等性的某些基本属性,例如对称性(a == bb == a),因为在第一种情况下,方法是正在被调用a,在第二种情况下b,由于OO的基本原理而被调用,这完全a是(在第一种情况下)的决定,或者b的决定(在第二种情况下)是否认为自己与另一人相等。获得对称性的唯一方法是让两个对象配合,但是如果它们不……运气不好。

一种解决方案是使相等性不是一个对象的属性,而是两个对象的属性或第三个对象的属性。后一种选择还解决了通用相等性的问题,因为如果将相等性设为第三个“上下文”对象的属性,则可以想象EqualityComparer针对不同上下文具有不同的对象。

例如,这为Haskell选择的具有Eq类型类的设计。它也是某些第三方Scala库(例如,ScalaZ)选择的设计,但不是Scala核心或标准库选择的,后者使用通用相等性与底层主机平台兼容。

有趣的是,它也是使用Java的Comparable/ Comparator接口选择的设计。Java的设计者显然意识到了这个问题,但是由于某种原因,它只能通过排序解决问题,而不能通过相等性(或哈希)解决。

所以,关于这个问题

为什么有一个Comparator接口,但不HasherEquator

答案是“我不知道”。显然,Java的设计者已经意识到了这个问题,这由的存在证明了Comparator,但是他们显然并不认为这对于相等性和哈希是一个问题。其他语言和库做出不同的选择。


7
+1,但请注意,有些OO语言存在多个调度(Smalltalk,Common Lisp)。因此,以下语句始终太强了:“在OO中,您总是在单个对象上调用方法”。
coredump

我找到了想要的报价;根据JLS 1.0,The methods equals and hashCode are declared for the benefit of hashtables such as java.util.Hashtable即Java equals和Java hashCode都是作为Object方法引入的,出于以下目的Hashtable-规范中的任何地方都没有UE或任何小题词,引文对我来说足够清楚;如果不是的话Hashtableequals可能会进入像这样的接口Comparable。因此,虽然我以前认为您的答案是正确的,但现在我认为这没有根据。
vaxquis's

@JörgWMittag这是一个错字,IFTFY。BTW说起clone-它最初是一个运算符,而不是一个方法(请参见Oak语言规范),引用:The unary operator clone is applied to an object. (...) The clone operator is normally used inside new to clone the prototype of some class, before applying the initializers (constructors)-三个类似关键字的运算符是instanceof new clone(第8.1节,运算符)。我认为这是clone/ Cloneablemess 的真实(历史)原因- Cloneable仅仅是后来的发明,而现有clone代码已经对其进行了改进。
vaxquis '16

2
“这是为Haskell选择的设计,例如,使用Eq类型类”。这是正确的,但值得注意的是,Haskell明确地预先声明了不同类型的两个对象永远不会相等,而Java的方法则并非如此。因此,相等操作是type的一部分(因此称为“ typeclass”),而不是第三上下文值的一部分。
杰克

19

真正的答案

为什么有一个Comparator接口,但不HasherEquator

是,由Josh Bloch提供

最初的Java API在紧迫的期限内很快就完成了,以迎接一个封闭的市场窗口。最初的Java团队做得非常出色,但是并非所有的API都是完美的。

问题仅仅在于Java的历史,与其他类似的问题,比如.clone()VS Cloneable

tl; dr

主要是出于历史原因;当前的行为/抽象是在JDK 1.0中引入的,后来没有得到解决,因为实际上不可能通过保持向后代码兼容性来做到这一点。


首先,让我们总结一些众所周知的Java事实:

  1. 从一开始到今天,Java一直向后兼容,要求旧版API在新版本中仍受支持,
  2. 因此,JDK 1.0引入的几乎每种语言构造都可以应用到今天,
  3. Hashtable.hashCode().equals()是在JDK 1.0,(实施哈希表
  4. Comparable/ Comparator是在JDK 1.2(Comparable)中引入的,

现在,它遵循:

  1. 在人们意识到存在比将它们放入超对象中更好的抽象之后,仍然要保持向后兼容性的同时.hashCode().equals()要对这些不同的接口进行改型和实现几乎是毫无意义的,因为例如,每个 1.2的Java程序员都知道每个人Object都有它们,并且保持物理状态以提供编译代码(JVM)兼容性-并为每个Object真正实现它们的子类添加显式接口将使这一混乱(原文如此!)等于Clonable一个(Bloch讨论了为什么Cloneable sucks很烂,也在EJ 2nd中进行了讨论和许多其他地方,包括SO),
  2. 他们只是将它们留在那儿,以便下一代拥有源源不断的WTF。

现在,您可能会问“这Hashtable一切都有什么?”

答案是:hashCode()/equals() 1995/1996年Java核心开发人员的合同和不太好的语言设计技能。

引用1996Java 1.0语言规范 -4.3.2 The Class Object,p.41:

声明这些方法equals和的目的hashCode是为了使哈希表受益,例如java.util.Hashtable(§21.7)。equals方法定义了对象相等性的概念,该概念基于值而不是引用的比较。

(注意这个确切的说法已经改变了在以后的版本,说,报价:The method hashCode is very useful, together with the method equals, in hashtables such as java.util.HashMap.,使其无法做出直接Hashtable- hashCode- equals不读历史JLS连接!)

Java团队决定他们想要一个好的字典样式的集合,并创建了Hashtable(到目前为止是个好主意),但是他们希望程序员能够以尽可能少的代码/学习曲线来使用它(糟糕!传入麻烦!)-并且,因为没有仿制药尚未[它的JDK 1.0毕竟],这将意味着,无论是每一个 Object投产Hashtable就必须明确地实现一些接口(和接口都还只是在其成立当年......没有Comparable然而,即使!) ,这Object会使许多人无法使用它-否则必须隐式实现某种哈希方法。

显然,出于上述原因,他们选择了解决方案2。是的,现在我们知道他们错了。...事后看来很聪明。轻笑

现在,hashCode() 要求具有它的每个对象都必须具有不同的equals()方法 -因此很明显也equals()必须将其放入Object

由于默认的上有效的方法的实现ab ObjectS被是多余的基本上是无用的(做a.equals(b) 等于a==ba.hashCode() == b.hashCode() 大致相等,以a==b还,除非hashCode和/或equals被重写,或者你GC几十万的Object应用程序的生命周期在S 1) ,可以肯定地说,它们主要是作为备份措施提供的,并且使用方便。如果您打算实际比较对象或对它们进行散列存储那么这就是我们始终熟知的事实的方式,即总是覆盖两者.equals().hashCode()。仅覆盖其中一个而不使用另一个是拧紧代码的好方法(通过糟糕的比较结果或疯狂的高存储桶碰撞值)-绕开它是初学者不断困惑和错误的根源(请搜索SO以查看) (适合您自己)和对经验丰富的人的持续干扰。

另外,请注意,尽管C#以更好的方式处理equals和hashcode,但Eric Lippert自己指出,他们在C#上犯的错误几乎与Sun在C#诞生之前对Java所犯的错误相同

但是,为什么每个对象都应该能够对其自身进行哈希处理以插入到哈希表中呢?要求每个对象都能够做似乎很奇怪。我认为,如果今天我们从头开始重新设计类型系统,则散列可能会以不同的方式进行,也许使用IHashable接口。但是,当设计CLR类型系统时,没有泛型类型,因此,通用哈希表需要能够存储任何对象。

当然1Object#hashCode仍然可以冲突,但是要花些力气才能做到这一点,请参见:http : //bugs.java.com/bugdatabase/view_bug.do?bug_id=6809470和链接的错误报告以获取详细信息;/programming/1381060/hashcode-uniqueness/1381114#1381114更深入地介绍了此主题。


不过,不只是Java。它的许多同时代人(Ruby,Python等)和前辈(Smalltalk等)以及一些后继者也具有普遍平等和普遍可哈希性(这是一个词吗?)。
W Mittag

@JörgWMittag看到programmers.stackexchange.com/questions/283194/... -我不同意关于Java中“UE”; 从历史上看,UE从来不是真正涉及到Object设计的人。哈希性为。
vaxquis's

@vaxquis我不想为此烦恼,但是我先前的评论显示,两个同时可访问的对象可以具有相同的(默认)哈希码。
恢复莫妮卡

1
@vaxquis好。我买 我担心的是,一个正在学习的人会看到这一点,并认为使用System哈希码(而不是等号)会变得很聪明。如果他们这样做,除了很少见的情况下,它可能会运行得很好,除非很少见。无法可靠地重现该问题。
JimmyJames

1
这应该是被接受的答案,因为被接受的答案的结论是“我不知道”
Phoenix,
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.