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

Scalaコップ本28章の続きをやっていきますよー。28章はオブジェクトの等価性についてやっておりますが、今回は実際の等価性定義に使えるサンプルを紹介していきますねー

equalsとhashCodeのレシピ

6章で取り扱った有理数を表現するRationalクラスをサンプルにして、いろんな状況で使えるであろうequalsとhashCodeの作り方を見ていきますよ

とりあえずサンプルの対象となるRationalクラスはこんな感じですねー

// パラメータとして分子、分母を取りますです
class Rational(n:int, d:Int){
  // 分母は0以外ですね
  require(d != 0)
  // 分子・分母の最大公約数を取得します
  private val g = gcd(n.abs, d.abs)
  // 正規化した分子を取得します(符号や数値の約分)
  val numer = if(if(d < 0) -n else n) / g
  // 正規化した分母を取得します(約分済) 
  val denom = d.abs / g
  // 最大公約数を計算する再帰処理です
  private def gcd(a:Int, b:int):Int = 
    if(b == 0) a else gcd(a, a % b)
    
  // equalsを再定義してやります
  override def equals (other:Any):Boolean = 
    other match {
      case that:Rational =>
        (that canEqual this) &&
        numer == that.numer &&
        denom == that.denom
      case _ => false
    }
    // ファイナルクラスではないのでcanEqualを定義します
    def canEqual(other:Any):Boolean =
      other.isInstanceOf[Rational]
    // hashCodeの再定義です
    override def hashCode:Int = 
      41 * (
        41 + numer
      ) + denom
      override def toString =
        if(denom == 1) numer.toString elese numer + "/" + denom
}

Rationalクラスはファイナルクラスではないのでequalsのオーバーライド時にcanEqualメソッド作る必要があるそうです。この基準は、ファイナルクラスでない、かつそのクラスの上位クラスでequalsを再定義していない(継承したequalsの定義がAnyRefのもの)場合はcanEqualが新しく必要、というものみたいです。ファイナルクラスでcanEqualが必要ないのはサブクラスが作られないため、equalsの再定義によるサブクラスの混乱が起こりえないからとのこと…詳しくはここらへんでやりました(`・ω・´)

それでは、等価性に関するそれぞれの要素をみていきますよ

canEqualメソッドの追加

まずcanEqualに渡される値はAny型である必要があるみたいです。またcanEqualでは渡されたオブジェクトが現在のクラスのインスタンス化どうかを判定しております。

 def canEqual(other:Any):Boolean =
      other.isInstanceOf[Rational]
equalsメソッド

equalsメソッドでは型パラメータをAnyで宣言する必要があるみたいです。またメソッドの本体はパターンマッチとなって、そのセレクターはパラメータに渡されるオブジェクト自身である必要があるとのことです。またパターンのケースは現在のクラスの型に対する型付きパターンかどうかで判定しますね(´・ω・`)

// 型パラメータはAnyです
override def equals (other:Any):Boolean = 
    // パターンマッチで等価性を判定します
    other match {
      // 同じ型かどうか判定するケースです
      case that:Rational =>
       // 同じクラス、かつ分母・分子が等しいかどうかで判定します
        (that canEqual this) &&
        numer == that.numer &&
        denom == that.denom
      // Rational以外の型なら一致させません
      case _ => false
    }

なお、オーバーライドしようとしているequalsがAnyRefの物でない場合はスーパークラスのequalsメソッドも実行したほうがいいみたいです。とりあえずこんな感じですかね。

  override def equals (other:Any):Boolean = 
    other match {
      case that:Rational =>
        // スーパークラスメソッドも実行します 
        super.equals(that) &&
        (that canEqual this) &&
        numer == that.numer &&
        denom == that.denom
      case _ => false
    }
hashCodeメソッド

hashCodeについては「Effective Java」がJavaクラスに対して推奨しているものに似た次のものをコップ本的にオススメするみたいです

override def hashCode:Int = 
      41 * (
        41 + numer
      ) + denom

オススメ計算順序については各関連フィールドを基に次のような計算になるみたいです

  • 最初のフィールドにハッシュコード41を加えて41を掛ける
  • 第2のフィールドにハッシュコードを加え、それに41を掛ける
  • 第3のフィールドのハッシュコードを加え、それに41を掛ける
  • ...(以下繰り返し)

例えばa, b, c, d, eの5つの関連フィールドをもつオブジェクトのハッシュコードは次のようになるみたいです

// ハイパー入れ子構造ですな(´・ω・`)
override def hashCode:Int = 
  41 * (
    41 * (
      41 * (
        41 * (
          41 + a.hashCode
        ) + b.hashCode
      ) + c.hashCode
    ) + d.hashCode
  ) + e.hashCode

ちなみにInt, Short, Byte, CharフィールドのhashCode呼び出しは(Intは値そのものを利用してShort, Byte, Charは値がIntに変換されるので)省略可能なので、a, b, c, d, eがIntの場合は次のように書き換えることが出来るみたいです。

override def hashCode:Int = 
  41 * (
    41 * (
      41 * (
        41 * (
          41 + a
        ) + b
      ) + c
    ) + d
  ) + e

それと乗算に使用している41は奇数の素数なのでオーバーフローによる情報の消失を最小限に抑えることができるから、最初のフィールド値(hashCode)に41を加えたのは最初の乗算が0になるのを防ぐためとのことです。

またequalsメソッドが判定式の中でsuper.equals(that)を利用している場合はhashCodeの中でもスーパークラスのhashCodeを呼び出す必要があるみたいです。Rational.hashCode内でスーパークラスメソッド呼び出しをするとこんな感じになりますね

override def hashCode:Int = 
      41 * (
          // スーパー呼び出しは0にならないはずなので
          // こんな感じで(´・ω・`)
          super.hashCode
        ) + numer
      ) + denom

なお、これまで定義してきたハッシュコードの性質は、オブジェクト関連フィールドがhashCodeを呼び出して手に入れるハッシュコード以上にはならないとのことですc⌒っ゚д゚)っφ メモメモ...

ちなみにScalaではIntやShortだけでなくList, Set, Map, Tupleなどでも格納されている要素を考慮した値を返すhashCodeが再定義されているので自然な形で使えるらしいのですが、用意Arrayについてはされていないようです。なので配列については個々の要素をフィールド的に扱ってやる or jva.util.ArraysシングルトンオブジェクトのhashCodeメソッドの中のどれかに配列を渡すことで対応する必要があるとのことです。

いじょー

とりあえず28章はここまでです。かなり写経風味な章だったのであとでやり直さないとな…ということで、次は29章ScalaJavaの結合に進みますねー