Chapter 30《Object Equality》

Scala中的相等性

  • Scala的相等性比较和Java中的不同,在Java中使用==表示两个对象的引用相等性,使用equals表示自然意义的相等性。在Scala中使用eq表示两个对象的引用相等性,equalsJavaObject类中的equals方法,使用==表示两个对象自然意义的相等性。对于新类的相等性,可以通过重写equals方法来实现==的相等意义。这个equals如果不重写,则还是使用的Object中的equals方法用来比较对象本身引用的相等性。==本身是不能被重写的,因为在Any类中该方法是被定义为final的。

对象相等性比较的设计

  • 由于大量的代码都依赖于对象的相等性,因此需要定义准确的相等性比较函数。刚开始定义容易出现的错误有:

    1. equals的签名定义错误
    2. 修改equals的时候没有修改hashCode
    3. 使用可变字段来定义equals
    4. 定义的equals不满足相等性关系

    1. equals的签名定义错误

    class Point(val x: Int, val y: Int) { ... }
    def equals(other: Point): Boolean = this.x == other.x && this.y == other.y
    

    定义p1p2equalstrue的两个点,当定义val p2a: Any = p2的时候,p1p2a使用equals时就不能返回true,原因在于在Point中定义的比较,使用的是Point类型的参数,而不是Any类型的,并没有Override Any类中的equals方法,而是对其的一个重载,ScalaJava一样也是使用编译器类型选择重载函数的版本,因此比较的使用的是Any中的原生equals,比较的是引用相等性,必然是不同的。HashSetContains方法中使用了对象的HashCode,和该对象在内存中的地址有关,所以尽管p1p2equals方法返回的是truep1也的确在set中,但是p1p2Hashcode是不一样的。

    改进版:

    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中,Hashcodeequals的修改总是同步的,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现在都是varequalshashCode都依赖于可变字段进行修改,所以当x,y改变的时候,相应的hashcode也会修改,因此,使用hashcode存储的coll会受到影响,因为原来的对象的key发生了变化,coll使用新的key寻找value时,会找不到。

4.不能满足相等关系

两个对象相等必须要满足以下的关系:自反性,对称性,传递性,重现性,任意不为nullvaluenull都是不相等的。目前的设计是满足这些关系的,但如果加入了子类,则情况发生了变化,定义了一个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]
    
    在实现equalshashcode时候,在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函数的步骤:

    1. non-final类中覆盖equals函数,并创建一个canEqual方法,如果是直接继承AnyRef,则在AnyRef中没有canEqual方法,此时创建的canEqual方法就是新的方法,否则的话,会重写继承类中的canEqual方法。唯一的例外是继承自AnyReffinal类,因为没有继承的子类,就算是定义了canEqual,也和没有定义是一样的,能调用 canEqual的对象调用该方法后的返回值都为true。因此不需要在这类类中定义canEqual函数,定义的签名如下所示:
      def canEqual(other: Any): Boolean =
    
    1. canEqual函数如果是当前类的一个实例,则需要返回true,否则的话返回false
      other.isInstanceOf[Rational]
    1. equals的重写,方法签名要写正确的:
      override def equals(other: Any): Boolean =
    1. 书写equals的方法体,使用一个match语句
      other match { // ... }
    1. match中的case分为两类,第一类是使用类型匹配,
      case that: Rational =>
    1. 接下来的判断逻辑:使用与逻辑表达式将各个单独的判断表达式并联在一起,如果需要使用父类中的equals方法,需要指明是super.equals而不是直接使用equals,如果定义了canEqual方法,使用它,
      (that canEqual this) &&
      最后使用类中的每一个域进行比较:
      numer == that.numer && denom == that.denom
    1. 最后一种情况,使用一个通配符进行匹配:
      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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容