【发布时间】:2011-09-10 09:48:18
【问题描述】:
在 Scala 中实现 equals 和 hashCode 方法的标准习惯用法是什么?
我知道在 Programming in Scala 中讨论了首选方法,但我目前无法访问这本书。
【问题讨论】:
-
该链接提供的建议已过时,现在不正确。请参阅下面我的“答案”以获取当前和正确的实现(截至 2.13)。 stackoverflow.com/a/56509518/501113
在 Scala 中实现 equals 和 hashCode 方法的标准习惯用法是什么?
我知道在 Programming in Scala 中讨论了首选方法,但我目前无法访问这本书。
【问题讨论】:
还有一个免费的第 1 版 PinS 也讨论了这个主题。但是,我认为最佳 来源是 Odersky 讨论 Java 中的平等的 this article。 PinS 中的讨论,iirc,是本文的缩略版。
【讨论】:
在做了相当多的研究之后,我找不到一个答案,它可以为 Scala 类(不是案例类,因为它是编译器自动生成的,并且should not be overridden)。我确实找到了一个 2.10 (stale) macro 勇敢地尝试解决这个问题。
我最终结合了"Effective Java, 2nd Edition" (by Joshua Bloch) 和本文中的"How to Write an Equality Method in Java" (by Martin Odersky, Lex Spoon, and Bill Venners) 中推荐的模式,并创建了一个标准的默认模式,我现在使用它来为我的Scala 类实现equals 和hashCode。
equals 模式的主要目标是最大程度地减少执行实际比较所需的次数,以达到有效且确定的true 或false。
此外,hashCode 方法应始终被覆盖并在 equals 方法被覆盖时重新实现(再次参见 "Effective Java, 2nd Edition" (by Joshua Bloch))。因此,我在下面的代码中包含了hashCode 方法“模式”,在实际实现中也包含了critical advice about using ## instead of hashCode。
值得一提的是,super.equals 和 super.hashCode 中的每一个都必须在祖先已经覆盖它时才被调用。如果不是,则必须不调用super.* 作为java.lang.Object 中的默认实现(equals compares for the same class instance 和hashCode most likely converts the memory address of the object into an integer),这两者都将破坏指定的equals 和hashCode 合约,用于现在被覆盖的方法.
class Person(val name: String, val age: Int) extends Equals {
override def canEqual(that: Any): Boolean =
that.isInstanceOf[Person]
//Intentionally avoiding the call to super.equals because no ancestor has overridden equals (see note 7 below)
override def equals(that: Any): Boolean =
that match {
case person: Person =>
( (this eq person) //optional, but highly recommended sans very specific knowledge about this exact class implementation
|| ( person.canEqual(this) //optional only if this class is marked final
&& (hashCode == person.hashCode) //optional, exceptionally execution efficient if hashCode is cached, at an obvious space inefficiency tradeoff
&& ( (name == person.name)
&& (age == person.age)
)
)
)
case _ =>
false
}
//Intentionally avoiding the call to super.hashCode because no ancestor has overridden hashCode (see note 7 below)
override def hashCode(): Int =
31 * (
name.##
) + age.##
}
代码有许多非常重要的细微差别:
scala.Equals - 确保equals 惯用模式(包括canEqual 方法形式化)得到全面实施。虽然扩展它在技术上是可选的,但仍然强烈推荐。(this eq person) 与 true 可确保不再进行(昂贵的)比较,因为它实际上是相同的实例。此测试必须在模式匹配内,因为eq 方法在AnyRef 上可用,而不是在Any(that 的类型)上可用。由于AnyRef 是Person 的祖先,因此该技术通过对后代Person 进行类型验证来同时进行两次类型验证,这意味着对其所有祖先(包括AnyRef)进行自动类型验证,这是必需的对于eq 检查。虽然此测试在技术上是可选的,但仍强烈推荐。that 的canEqual - 很容易将其倒退,这是不正确的。在that 实例上执行canEqual 的检查至关重要,this 作为参数提供。虽然它对于模式匹配似乎是多余的(鉴于我们得到这行代码,that 必须是一个 Person 实例),我们仍然必须进行方法调用,因为我们不能假设 that 是一个等于- Person 的兼容后代(Person 的所有后代将成功地模式匹配为 Person)。如果该类标记为final,则此测试是可选的并且可以安全地删除。否则,它是必需的。hashCode 短路 - 虽然不充分也不需要,但如果此hashCode 测试为false,则无需执行所有值级别检查(第 5 项)。如果这个测试是true,那么实际上需要逐个字段检查。此测试是可选的,如果 hashCode 值未缓存并且每个字段相等检查的总成本足够低,则可能会被排除在外。hashCode 测试并成功,仍然必须检查所有字段级别的值。这是因为,尽管可能性极小,it remains possible for two different instances to generate the exact same hashCode value, and still not be actually equivalent at the field level。还必须调用父级的 equals 以确保在父级中定义的任何其他字段也被测试。case _ => - 这实际上实现了两种不同的效果。首先,Scala 模式匹配保证null 在此处正确路由,因此null 不必出现在我们纯Scala 代码中的任何位置。其次,模式匹配保证无论that 是什么,它都不是Person 或其后代之一的实例。super.equals 和 super.hashCode 有点棘手 - 如果祖先已经覆盖了这两个(不应该是其中任何一个),则必须将 super.* 合并到您自己的覆盖实现中。如果一个祖先没有覆盖两者,那么你覆盖的实现必须避免调用super.*。上面的Person 代码示例显示了没有祖先 覆盖两者的情况。因此,调用每个 super.* 方法调用将错误地一直落入默认的 java.lang.Object.* 实现,这将使 equals 和 hashCode 的假定组合合同无效。这是基于super.equals 的代码,仅当至少有一个祖先已明确覆盖equals 时才使用。
override def equals(that: Any): Boolean =
...
case person: Person =>
( ...
//WARNING: including the next line ASSUMES at least one ancestor has already overridden equals; i.e. that this does not end up invoking java.lang.Object.equals
&& ( super.equals(person) //incorporate checking ancestor(s)' fields
&& (name == person.name)
&& (age == person.age)
)
...
)
...
这是基于super.hashCode 的代码,仅当至少有一个祖先已明确覆盖hashCode 时才使用。
override def hashCode(): Int =
31 * (
31 * (
//WARNING: including the next line ASSUMES at least one ancestor has already overridden hashCode; i.e. that this does not end up invoking java.lang.Object.hashCode
super.hashCode //incorporate adding ancestor(s)' hashCode (and thereby, their fields)
) + name.##
) + age.##
最后一点:在我为此进行研究时,我无法相信这种模式存在多少错误的实现。显然,这仍然是一个难以准确把握细节的领域:
hashCode 覆盖和实现时,在类字段上错误地使用 .hashCode 而不是 .##。参见 Tree3.scala【讨论】:
是的,覆盖 equals 和 hashCode 在 Java 和 Scala 中都是一项艰巨的任务。我建议根本不要使用equals,而是使用类型类(Eq/Eql 等)。它更加类型安全(比较不相关类型时出现编译器错误),更易于实现(没有覆盖和类检查)并且更灵活(您可以编写与数据类分开的类型类实例)。 Dotty 使用了 "multiversal equality" 的概念,它提供了在捕捉 equals 的一些明显不正确的用法和严格相等检查(如 Haskell)之间的选择。
【讨论】:
java.lang.Object 的equals 和hashCode 方法。