问题描述
在阅读了“ Scala编程”一书的第30章有关对象相等性的示例之后,我对如何在通用容器中确保equals
方法的反射性感到有些困惑,考虑到NaN比较是不自反的。请考虑以下代码段:
class Wrapper[T](val elem: T) {
override def equals(other: Any): Boolean = other match {
case that: Wrapper[_] => this.elem == that.elem
case _ => false
}
}
object NaNCompare extends App {
val nan: Double = 0.0 / 0.0
val nanw: Wrapper[Double] = new Wrapper(nan)
val pzero: Double = +0.0
val pzerow: Wrapper[Double] = new Wrapper(pzero)
val mzero: Double = -0.0
val mzerow: Wrapper[Double] = new Wrapper(mzero)
println(s"nan equals nan: ${nan equals nan}")
println(s"nan == nan: ${nan == nan}")
println(s"+0 equals -0: ${pzero equals mzero}")
println(s"+0 == -0: ${pzero == mzero}")
println(s"[nan] equals [nan]: ${nanw equals nanw}")
println(s"[nan] == [nan]: ${nanw == nanw}")
println(s"[+0] equals [-0]: ${pzerow equals mzerow}")
println(s"[+0] == [-0]: ${pzerow == mzerow}")
}
这将打印以下内容:
nan equals nan: true
nan == nan: false
+0 equals -0: false
+0 == -0: true
[nan] equals [nan]: true
[nan] == [nan]: true
[+0] equals [-0]: true
[+0] == [-0]: true
前四行是逻辑上的:根据IEEE 754,nan == nan
为假,而+0 == -0
为真;但是,两个nan是同一个对象,因此nan equals nan
必须满足equals
的反射性要求; +0 equals -0
为假,因为这两个浮点数具有不同的表示形式。到目前为止一切顺利。
但是,Wrapper
被通用==
包裹时,在比较NaN时突然开始生成true
。我首先以为这是由于类型擦除导致的,所以它基本上比较位表示(对于相同的NaN来说是相等的),但是如果是这样,则它必须在比较加零和减零的包装时打印false
。但是,这两种情况都是正确的。
为了使您更加困惑,如果在代码的第一行(Double
)的上一行添加class Wrapper[T <: Double](val elem: T) {
,它将显示以下内容:
nan == nan: false
+0 equals -0: false
+0 == -0: true
[nan] equals [nan]: false
[nan] == [nan]: false
[+0] equals [-0]: true
[+0] == [-0]: true
因此,如果它受Double
的限制,则包裹的NaN不再相等!如果用AnyVal
绑定,则它们与原始代码中的相等。如果使包装器为非通用包装器(删除T参数并用Double
代替),则它们是不相等的。因此很明显,在不同情况下,编译器“记住” Double
的内部结构。但是,它到底能记住什么以及如何执行==
的分派?
解决方法
我怀疑正在发生的事情是Scala可以使用两种不同的IEEE双精度运行时实现,并且它们的相等语义略有不同。
- 对于Java原语
double
,比较是使用特定的JVM指令实现的;+0.0 == -0.0
为true
,而Double.NaN == Double.NaN
为假 - 对于
java.lang.Double
(Object
的{{1}}框),double
是通过比较位表示来实现的,这导致equals
比较{{1 }}和NaN
不等于true
。
在Scala中,只要证明它正在处理+0
(并且没有-0
的方法都可以使用),编译器就基本上会使用原语double
(因为它快得多)。被称为,例如scala.Double
,并且在不确定时将使用java.lang.Object
(例如,在通用名称(尚未使用equals
...)中。 Scala java.lang.Double
方法并不是@specialized
的同义词,因为它对在JVM中作为原语实现的事物有特殊的处理。
通常,应将Scala代码中==
的用法替换为.equals
。