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


Scalaコップ本の28章に進んでいきますよー、28章はオブジェクトの等価性についてやっていきますね(`・ω・´)

Scalaにおける等価性

JavaScalaの違いの1つとして等価性の定義が違うことが挙げられるみたいです。Javaでは==演算子による(値型では自然な、参照型では参照)等価性の比較と、equalsメソッドによる参照型の値等価性の2つがあったりしますデス。具体的には、例えばx, yの2つの文字列が同じ順序で同じ文字を含んでいた場合にx == yはオブジェクトが異なるのでfalseになるのに対して、x equals yはtrueになるみたいな違いがあるらしいです。

Scalaの等価性

これに対してScalaには参照等価性をテストするメソッドとしてx eq yがあるもののほとんど使われないで、==メソッドによる全ての型での「自然な」等価性の判定が使われるみたいです。この==メソッドは値型ではJavaの==と同じ動作、参照型ではequalsと同じ意味で使われるみたいです。なので、新しい型の==の動作はequalsメソッドをオーバーライドして再定義するのだとか。

equalsメソッド

equals(==も)メソッドはAnyから継承されるメソッドでデフォルトではeq(参照等価性)的な動作をするみたいです。なのでequalsの動作を変える場合はオーバライドする必要があるみたいです。なお==メソッドはAnyでファイナルメソッド宣言されているので、直接オーバーライドすることができないみたいです(´・ω・`)

ちなみにAnyクラスでは次のように==メソッドを定義しているみたいです。

// ファイナル宣言されておりますな
final def == (that:Any):Boolean = 
  // nullかどうかで処理分けるんですね( ´∀`)<ぬるぽ
  if(null eq this){ null eq that } else {this equals that}

等価メソッドの開発

オブジェクト指向言語で正しい等価メソッドを書くのは難しいという結論が2007年くらいの論文で発表されているらしいですな。なにやら膨大な数のJavaコードを研究した結果、ほとんどの実装が誤ってたとのこと…そんな研究もあるのか(´・ω・`)

等価性は多くの処理の基本になっているのでこの事実は大変な感じですな。なのでequalsのオーバーライドして実装する場合は注意いなければいけないとのことです。コップ本的には次の4つのミスに特に注意しましょう、とのことですな(`・ω・´)

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

それぞれについて詳しく見ていきますよ

誤ったシグネチャーでequalsを定義するミス

ミスのサンプルとして次のような点を表現するクラスに等価メソッドを追加してみます。

class Point(val x:Int, val y:Int) {
  // これは間違っています
  def equals(other:Point):Boolean =
    this.x == other.x && this.y == other.y
}

それでは間違い動作を追っていきますよ

// 同じポイントオブジェクトを取得します
scala> val p1, p2 = new Point(1,2)
p1: Point = Point@b5ac2c
p2: Point = Point@13a95af

// 上とは異なるポイントオブジェクトです
scala> val q = new Point(2,3)
q: Point = Point@149249e

//// 単純比較します
// 同じですね
scala> p1 equals p2
res0: Boolean = true
// 異なりますね
scala> p1 equals q 
res1: Boolean = false

//// コレクションにつっこんでみます
scala> import scala.collection.mutable._
import scala.collection.mutable._

scala> val coll = HashSet(p1)
coll: scala.collection.mutable.Set[Point] = Set(Point@b5ac2c)

// p1との比較なのにfalseになりました(´・ω・`)
scala> coll contains p2
res2: Boolean = false

単純比較ではうまくいってたのに、コレクションに突っ込んだ瞬間破綻してしまいました(´;ω;`)これは標準のequalsをオーバーライドしていないのでAny型をうまく処理出来ていないためっぽいです。具体的な例としてはこんなふうな比較に失敗しているわけですな

// p2:Pointをp2a:Anyに変換します
scala> val p2a:Any = p2
p2a: Any = Point@13a95af

// 元々等しいのに
scala> p1 equals p2
res3: Boolean = true

// Any型にするとだめです
scala> p1 equals p2a
res4: Boolean = false

今回の定義ではequalsのオーバーライドしていないのでequalsのパラメータはPoint型しかとらないため、p2a:Anyを引数としたことでAny型(大元)のequalsが呼ばれて比較に失敗した…と。なので完全ではないですがちょろっと修正してみます。

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
  }
}

んじゃ試してみますよ

scala> val p1, p2 = new Point(1,2)
p1: Point = Point@1e3e1b8
p2: Point = Point@1f81efb

scala> val p2a:Any = p2
p2a: Any = Point@1f81efb

// Any型にしてもきっちり評価できました。
scala> p1 equals p2
res5: Boolean = true

ちなみに似た様なミスとしては誤ったシグネチャーで==メソッドを定義する、というのがあるみたいですね。==メソッドはAny型でファイナルメソッドとして定義されているので次のよな定義であればコンパイル時に怒られますです

def  ==(other:Any):Boolean = /...

しかしながら深く考えずに間違ったシグネチャーで定義してしまうとコンパイルが通ってしまうので、後々変なところではまってしまうことになるみたいです。

// コンパイルが通ってしまうので余計に厄介なミスです
def  ==(other:Point):Boolean = /...

これはScalaの多重定義できるというメリットがダメな方に働いた例になるわけなので、初心者は特に気をつけましょう…とのことです、了解です(´・ω・`)

いじょー

残念ながら時間切れです。次回は2番目のポイントのhashCodeに変更を加えないミスからやっていきますよ