LL脳がscalaの勉強を始めたよ その96


Scalaコップ本の28章の続きをやっていきますよ。28章はオブジェクトの等価性についてのエトセトラです。前回は等価性評価メソッドequalsのオーバーライドをする際の注意点の途中で終わってしまったので、今回はその続きからやっていきたいと思います。

等価メソッドの開発(続き)

equalsオーバーライド時の注意点は次の4つでしたが、今回は2番目のhashCodeに変更を加えないミスから見ていくことにしますねー

  • 誤ったシグネチャーでequalsを定義(済み)
  • hashCodeに変更を加えずにequalsだけを定義
  • ミュータブルなフィールドによってequalsを定義
  • 等価関係を表すものとしてequalsを定義出来ていない

ちなみに前回サンプルとして利用した点座標を表現するオブジェクトPointを今回も使っていきますよ

class Point(val x:Int, val y:Int) {
  // 完全ではないですがオーバーライドします
  override def equals(other:Any) = other match {
    // Point型の場合は座標ベースで等価比較をします
    case that:Point => this.x == that.x && this.y == that.y
    // ソレ以外はFalseが返ります
    case _ => false
  }
}
hashCodeに変更を加えずにequalsだけを定義

まずは前回定義したPointオブジェクトのequalsメソッドを実行確認してみますかね

scala> val p1, p2 = new Point(1,2)
p1: Point = Point@a7bd7a
p2: Point = Point@88a970

// 同じオブジェクトの比較ですな
scala> p1 == p2                   
res2: Boolean = true

// Any型でも対応しますよ
scala> val p2a:Any = p2
p2a: Any = Point@88a970

scala> p1 == p2a
res3: Boolean = true

...と、前回やったAny型に変換されたPointオブジェクトの等価比較にも対応できています…が、HashSet.containsを使った等価比較は次のように失敗していしまいます。

scala> import scala.collection.mutable._
import scala.collection.mutable._
// HashSetの要素にp2と同じものが含まれているかテストします
scala> HashSet(p1) contains p2
// が失敗しました(´・ω・`)
res4: Boolean = false

どうやら前回定義したequalsではまだ足りないようです。ちなみに上記結果は必ずfalseが帰るわけではなく、trueが返される場合もあるとのこと(´・ω・`)なおのこと駄目じゃん。

上記のようになるのはPointの中でHashCodeの再定義を行わずにequalsのみを再定義しているからだそうです。これはHashCodeを定義していないので、PointオブジェクトでHashCodeを使った場合はAnyRefのモノが使用される→オブジェクトのアドレスが変換される可能性がある…ということみたいです。型変換されるからかなあ(´・ω・`)?

とりあえずHashSetのcontainsはハッシュコードによって決まる対象のハッシュバケットを判断してから、パケット内のすべての要素と引数の要素を比較するような処理をするみたいです。つまり同じハッシュバケット内にないと上手く比較ができない、みたいな感じですかね。なのでPointオブジェクトのp1とp2のハッシュコードが異なるだろうことから、高い確率で異なるハッシュバケットに振り分けられる→contains時にp1とp2がマッチ出来ない(´;ω;`)という事になるみたいデス。また、偶然p1, p2がおアンじハッシュバケットに入っていればtrueが返ったりするとか。

上記のような動作はAnyクラスで定義されたHashCodeについての次のような決まりをPointクラスの実装が破ってしまったことによるものみたいです。

2つのオブジェクトがequalsメソッドの基準dね等しい場合、2つのオブジェクトをレシーバーとしてhashCodeを呼び出したときの結果値は同じ整数値でなければならない

...とりあえず同じ座標のものは同じ整数値をもつようにhashCodeを定義してやればいいですかね?

…ということでPointクラスのhashCodeを再定義したのが次のものになりますね。

class Point(val x:Int, val y:Int){
  // 同じ座標のものは同じHashCodeになるように再定義します
  // 変換式は例ですな、適当にバラけるようにつくればいいみたいです。
  override def hashCode  = 41 * (41 + x) + y
  override def equals(other:Any) = other match {
    case that:Point => this.x == that.x && this.y == that.y
    case _ => false
  }
}

では上記コードを実行して確認してみますかね(`・ω・´)

scala> val p1, p2 = new Point(1,2)
p1: Point = Point@6bc
p2: Point = Point@6bc

scala> HashSet(p1) contains p2
res5: Boolean = true

おお!うまくいきました

ミュータブルなフィールドによってequalsを定義するミス

3つめのミスを検証するために先ほど定義したPointクラスの座標値をvarで置き換えてみます

class Point(var x:Int, var y:Int){
  override def hashCode  = 41 * (41 + x) + y
  override def equals(other:Any) = other match {
    case that:Point => this.x == that.x && this.y == that.y
    case _ => false
  }
}

とりあえず簡単な動作検証です

scala> val p = new Point(1,2)
p: Point = Point@6bc

scala> val coll = HashSet(p)
coll: scala.collection.mutable.Set[Point] = Set(Point@6bc)

// HashSetのcontains評価はうまくいきますね
scala> coll contains p
res7: Boolean = true

では、var定義なのでpのフィールドの値を書き換えたうえで評価してみます

scala> p.x += 1

// もともと同じPointオブジェクトなのにフィールドが書き換えられたらfalseになりました
scala> coll contains p      
res9: Boolean = false

// でもイテレータで各要素ごとに評価するとtrueになる不思議(´・ω・`)
scala> coll.elements contains p
res10: Boolean = true

なんだかへんな動作をしているのですが、これはpのフィールド値が変更されたときにhasCodeの値が変わってしまいハッシュバケットの振り分けが変更される→contains評価に失敗する、という流れみたいです。コップ本的表現としてハッシュバケットが変わる→「HashSetの視界から外れる」ということみたいですが、なるほどc⌒っ゚д゚)っφ メモメモ...

上記の内容を乱暴にまとめると、いろんな処理の基板になっている等価評価で値が変わる(影響のでかい)いミュータブルは使うんじゃねぇよ(#゚Д゚)ってことですかね。もしもvarを使って値が変更できるオブジェクトにおいて内部状態を考慮した比較が必要なら、例えば次のように別メソッドで定義するのがコップ本的オススメみたいです。

class Point(var x:Int, var y:Int){
  //hashCode, equalsはオーバーライドしません
  // かわりにequalsとは異なる等価比較メソッドを用意してやります
  def equalsContent(other:Any) = other match {
    case that:Point => this.x == that.x && this.y == that.y
    case _ => false
  }
}

上記のようにすればPointオブジェクトはhashCode, equalsはデフォルトのものを使用するのでフィールドの値を変更したあとでも見つかるようになるみたいですね

scala> val p = new Point(1,2)
p: Point = Point@179567c

scala> val coll = HashSet(p)
coll: scala.collection.mutable.Set[Point] = Set(Point@179567c)

scala> coll contains p
res11: Boolean = true

scala> p.x += 1

//フィールドの値を変えても同じオブジェクトを認識できました
scala> coll contains p
res13: Boolean = true

いじょー

時間切れで全部やれませんでした(´・ω・`)ので次回は4つ目の等価関係を表すものとしてequalsを定義出来ていないミスからやっていきますねー、この章は結構苦手な感じだけどもがんばりまっす