Chapter 30《Object Equality》
Scala中的相等性
-
Scala
的相等性比较和Java
中的不同,在Java
中使用==
表示两个对象的引用相等性,使用equals
表示自然意义的相等性。在Scala
中使用eq
表示两个对象的引用相等性,equals
是Java
的Object
类中的equals
方法,使用==
表示两个对象自然意义的相等性。对于新类的相等性,可以通过重写equals
方法来实现==
的相等意义。这个equals
如果不重写,则还是使用的Object
中的equals
方法用来比较对象本身引用的相等性。==
本身是不能被重写的,因为在Any
类中该方法是被定义为final
的。
对象相等性比较的设计
-
由于大量的代码都依赖于对象的相等性,因此需要定义准确的相等性比较函数。刚开始定义容易出现的错误有:
-
equals
的签名定义错误 - 修改
equals
的时候没有修改hashCode
- 使用可变字段来定义
equals
- 定义的
equals
不满足相等性关系
1. equals的签名定义错误
class Point(val x: Int, val y: Int) { ... } def equals(other: Point): Boolean = this.x == other.x && this.y == other.y
定义
p1
和p2
为equals
为true
的两个点,当定义val p2a: Any = p2
的时候,p1
和p2a
使用equals
时就不能返回true
,原因在于在Point
中定义的比较,使用的是Point
类型的参数,而不是Any
类型的,并没有Override Any
类中的equals
方法,而是对其的一个重载,Scala
和Java
一样也是使用编译器类型选择重载函数的版本,因此比较的使用的是Any
中的原生equals
,比较的是引用相等性,必然是不同的。HashSet
的Contains
方法中使用了对象的HashCode
,和该对象在内存中的地址有关,所以尽管p1
和p2
的equals
方法返回的是true
,p1
也的确在set
中,但是p1
和p2
的Hashcode
是不一样的。改进版:
override def equals(other: Any) = other match { case that: Point => this.x == that.x && this.y == that.y case _ => false }
重写了
equals
方法,任何两个对象的equals
都会使用这个版本。不要试图去定义==
方法。2.修改了equals但是不修改HashCode
以上的
HashSet
中的Contains
方法结果出错的原因就是没有重新定义HashCode
,使用Hash
桶存储数据。HashCode
的实现依然是AnyRef
中的实现,和对象的地址有关。equals
相等,但是contains
找不到,这样是不合逻辑的。所以一般在修改了equals
的类中,也会修改Hashcode
使其相等。在Java
中,Hashcode
和equals
的修改总是同步的,hashcode
的值依赖于equals
比较时使用的类的fields
。例如可以这样定义class Point(val x: Int, val y: Int) { override def hashCode = (x, y).## override def equals(other: Any) = other match { case that: Point => this.x == that.x && this.y == that.y case _ => false } }
##
是计算hash code
的简写方法。3.equals依赖于可变的类字段,例如
class Point(var x: Int, var y: Int) { // Problematic override def hashCode = (x, y).## override def equals(other: Any) = other match { case that: Point => this.x == that.x && this.y == that.y case _ => false } }
x,y
现在都是var
,equals
和hashCode
都依赖于可变字段进行修改,所以当x,y
改变的时候,相应的hashcode
也会修改,因此,使用hashcode
存储的coll
会受到影响,因为原来的对象的key
发生了变化,coll
使用新的key
寻找value
时,会找不到。 -
4.不能满足相等关系
两个对象相等必须要满足以下的关系:自反性,对称性,传递性,重现性,任意不为null
的value
和null
都是不相等的。目前的设计是满足这些关系的,但如果加入了子类,则情况发生了变化,定义了一个ColoredPoint
。
class ColoredPoint(x: Int, y: Int, val color: Color.Value)
extends Point(x, y) { // Problem: equals not symmetric
override def equals(other: Any) = other match {
case that: ColoredPoint =>
this.color == that.color && super.equals(that)
case _ => false
}
}
该子类继承了父类的hashCode
并在equals
中调用了父类的equals
方法。p = new Point(1,2),cp = new ColoredPoint(1,2,Yellow)
,p equals cp
可以返回正确的true
,但是cp equals p
返回的却是false
,因为p
并不是一个ColoredPoint
,不满足对称性。将equals
中的对比条件设置得更宽泛些,因此有了以下的设置:
class ColoredPoint(x: Int, y: Int, val color: Color.Value) extends Point(x, y) { // Problem: equals not transitive
override def equals(other: Any) = other match {
case that: ColoredPoint =>
(this.color == that.color) && super.equals(that)
case that: Point =>
that equals this
case _ =>
false
}
}
这样可以满足对称性,但是不满足传递性!p1 == cp1(RED)
,p1 == cp2(BLUE)
,但是cp1(RED)
和cp2(BLUE)
并不是相等的。如何能够在类的层级上定义相等函数并保持同级相等关系需要满足的条件?使用canEqual
函数
def canEqual(other: Any): Boolean
在任何重写了equals
的类中都定义一下这个函数,这个函数定义了哪些obj
可以被当做该类的一个实例,从而返回true
,否则的话返回false
。这个方法会在equals
中被调用,从而满足子类进行对比的要求。以下版本是特别严格的比较版本:
class Point(val x: Int, val y: Int) {
override def hashCode = (x, y).##
override def equals(other: Any) = other match {
case that: Point => (that canEqual this) && (this.x == that.x) && (this.y == that.y)
case _ => false
}
def canEqual(other: Any) = other.isInstanceOf[Point]
}
canEqual
声明了所有的Point
的子类都可以被当做是Point
进行比较。对于ColoredPoint
的实现则可以
class ColoredPoint(x: Int, y: Int, val color: String) extends Point(x, y) {
override def hashCode = (super.hashCode, color).##
override def equals(other: Any) = other match {
case that: ColoredPoint => (that canEqual this) && super.equals(that) && this.color == that.color
case _ => false
}
override def canEqual(other: Any) =
other.isInstanceOf[ColoredPoint]
}
如果在父类中重写了equals
并定义了canEqual
函数,那么在子类中可以决定要不要定义canEqual
函数来决定子类对象是否可以和父类相等。如果定义了canEqual
,则子类对象不能和父类对象相等;如果没有定义canEqual
,则子类继承父类的canEqual
,子类对象可以和父类对象相等,这种情况发生在子类对象和父类对象真的可以相等的时候,因为相等具有自反性,child=father,father=child
则是必须的。这种情况则最大给予了子类的自由,如果是一个new Point(1,2) {val y=1}
这种,其实就是父类,可以不用重写canEqual
,但是如果在Point
上添加了色彩属性,就一般需要重新覆盖canEqual
了。
为参数化的类型定义相等性
- 之前做
equals
的第一步就比较equals
的操作数类型是否为含有equals
的类的实例,当这个类含有类型参数的时候,情况会稍微复杂一些。
在实现trait Tree[+T] { def elem: T def left: Tree[T] def right: Tree[T] } object EmptyTree extends Tree[Nothing] { def elem = throw new NoSuchElementException("EmptyTree.elem") def left = throw new NoSuchElementException("EmptyTree.left") def right = throw new NoSuchElementException("EmptyTree.right") } class Branch[+T](val elem: T, val left: Tree[T], val right: Tree[T]) extends Tree[T]
equals
和hashcode
时候,在Tree
类中并不需要实现,只需在子类中进行实现即可。在EmptyTree
对象中,继承自AnyRef
的默认实现就可以了。毕竟,一个object
就应该等于自己,比较的也就是引用地址的相等性。在Branch
中实现的时候,需要当前的元素是相等的,左子树是相等的,右子树也是相等的。如果在equals
中采用这样的写法:
如果使用override def equals(other: Any) = other match { case that: Branch[T] => this.elem == that.elem && this.left == that.left && this.right == that.right case _ => false }
uncheck
选项,会有uncheck
的警告,因为泛型信息在编译器被擦除,编译器只能检测出对象是否为Branch
,而不能检测出是否为Branch[T]
,这个T
在运行时期是不可见的。class Branch[T](val elem: T, val left: Tree[T], val right: Tree[T]) extends Tree[T] { override def equals(other: Any) = other match { case that: Branch[_] => (that canEqual this) && this.elem == that.elem && this.left == that.left && this.right == that.right case _ => false } def canEqual(other: Any) = other.isInstanceOf[Branch[_]] override def hashCode: Int = (elem, left, right).## }
制定equals函数的步骤:
-
- 在
non-final
类中覆盖equals
函数,并创建一个canEqual
方法,如果是直接继承AnyRef
,则在AnyRef
中没有canEqual
方法,此时创建的canEqual
方法就是新的方法,否则的话,会重写继承类中的canEqual
方法。唯一的例外是继承自AnyRef
的final
类,因为没有继承的子类,就算是定义了canEqual
,也和没有定义是一样的,能调用canEqual
的对象调用该方法后的返回值都为true
。因此不需要在这类类中定义canEqual
函数,定义的签名如下所示:
def canEqual(other: Any): Boolean =
- 在
-
canEqual
函数如果是当前类的一个实例,则需要返回true
,否则的话返回false
other.isInstanceOf[Rational]
-
- 对
equals
的重写,方法签名要写正确的:
override def equals(other: Any): Boolean =
- 对
- 书写
equals
的方法体,使用一个match
语句
other match { // ... }
- 书写
-
match
中的case
分为两类,第一类是使用类型匹配,
case that: Rational =>
-
- 接下来的判断逻辑:使用与逻辑表达式将各个单独的判断表达式并联在一起,如果需要使用父类中的
equals
方法,需要指明是super.equals
而不是直接使用equals
,如果定义了canEqual
方法,使用它,
(that canEqual this) &&
最后使用类中的每一个域进行比较:
numer == that.numer && denom == that.denom
- 接下来的判断逻辑:使用与逻辑表达式将各个单独的判断表达式并联在一起,如果需要使用父类中的
- 最后一种情况,使用一个通配符进行匹配:
case _ => false
- 最后一种情况,使用一个通配符进行匹配:
制定hashcode的步骤
-
hashcode
的制定依赖于判断相等性的字段,使用元组将这些字段组合起来,然后使用##
计算其hashcode
,如果在equals
中调用了super.equals
,则在hashcode
中也必须算上override def hashCode: Int = (super.hashCode, numer, denom).##
总结
- 实现一个正确的
equals
方法必须小心equals
的签名,重写hashCode
,不能依赖变化的字段,如果不是final class
的话,还需要实现canEqual
方法。因此一般如果需要比较的类,推荐使用case class
。