case classで自動的に実装されるhashCodeについて

  • 0
    いいね
  • 0
    コメント
    この記事は最終更新日から1年以上が経過しています。

    case classを使うとequalshashCodeが自動的に実装されます。
    参考:ひしだまさんのScalaクラスメモ
    hashCodeの実装は具体的にどんなコードが生成されているか、が気になって調べました。

    注意:概要程度分かれば十分だったので、そこまで詳細に踏み入ってないです。

    調べ方

    Stack Overflowの回答のように-Xprint:typerを付けることでREPLでも簡単に確認することができます。
    なお、SBTコンソールで設定したい場合はset scalacOptions in (Compile, console) := "-Xprint:typer"を指定できます。

    サンプル

    Scala 2.11.8

    プリミティブでない場合

    case class A(x: Seq[String])
    

    だと、こんな感じになります。

    case class A extends AnyRef with Product with Serializable {
      <caseaccessor> <paramaccessor> private[this] val x: Seq[String] = _;
      <stable> <caseaccessor> <accessor> <paramaccessor> def x: Seq[String] = A.this.x;
      def <init>(x: Seq[String]): A = {
        A.super.<init>();
        ()
      };
      <synthetic> def copy(x: Seq[String] = x): A = new A(x);
      <synthetic> def copy$default$1: Seq[String] = A.this.x;
      override <synthetic> def productPrefix: String = "A";
      <synthetic> def productArity: Int = 1;
      <synthetic> def productElement(x$1: Int): Any = x$1 match {
        case 0 => A.this.x
        case _ => throw new IndexOutOfBoundsException(x$1.toString())
      };
      override <synthetic> def productIterator: Iterator[Any] = runtime.this.ScalaRunTime.typedProductIterator[Any](A.this);
      <synthetic> def canEqual(x$1: Any): Boolean = x$1.$isInstanceOf[A]();
      override <synthetic> def hashCode(): Int = ScalaRunTime.this._hashCode(A.this);
      override <synthetic> def toString(): String = ScalaRunTime.this._toString(A.this);
      override <synthetic> def equals(x$1: Any): Boolean = A.this.eq(x$1.asInstanceOf[Object]).||(x$1 match {
        case (_: A) => true
        case _ => false
        }.&&({
          <synthetic> val A$1: A = x$1.asInstanceOf[A];
          A.this.x.==(A$1.x).&&(A$1.canEqual(A.this))
        }))
    };
    <synthetic> object A extends scala.runtime.AbstractFunction1[Seq[String],A] with Serializable {
      def <init>(): A.type = {
        A.super.<init>();
        ()
      };
      final override <synthetic> def toString(): String = "A";
      case <synthetic> def apply(x: Seq[String]): A = new A(x);
      case <synthetic> def unapply(x$0: A): Option[Seq[String]] = if (x$0.==(null))
        scala.this.None
      else
        Some.apply[Seq[String]](x$0.x);
      <synthetic> private def readResolve(): Object = $iw.this.A
    }
    

    hashCodeの部分は

    override <synthetic> def hashCode(): Int = ScalaRunTime.this._hashCode(A.this);
    

    となってます。このScalaRuntimeはscala.runtimeパッケージに含まれます。
    どんな実装になっているかというとdef _hashCode(x: Product): Int = scala.util.hashing.MurmurHash3.productHash(x)と更にhashingパッケージのメソッドを呼び出します。
    その中身だけ抜粋すると、こうなっています。

    final val productSeed = 0xcafebabe
    def productHash(x: Product): Int = productHash(x, productSeed)
    
    /** Compute the hash of a product */
    final def productHash(x: Product, seed: Int): Int = {
      val arr = x.productArity
      // Case objects have the hashCode inlined directly into the
      // synthetic hashCode method, but this method should still give
      // a correct result if passed a case object.
      if (arr == 0) {
        x.productPrefix.hashCode
      }
      else {
        var h = seed
        var i = 0
        while (i < arr) {
          h = mix(h, x.productElement(i).##)
          i += 1
        }
        finalizeHash(h, arr)
      }
    }
    

    productElementで逐次##を呼んでいます。(後述しますが##だとnullが来ても落ちない。)
    ちなみに、ListのhashCodeはどう実装されているかというと、LinearSeqLikeのなかで

    override def hashCode()= scala.util.hashing.MurmurHash3.seqHash(seq) // TODO - can we get faster via "linearSeqHash" ?
    

    という記述が見つかります。(コレクションは継承関係が複雑なので見誤っている可能性アリ)
    このseqHashの中は、まぁ、予想通りで1要素ずつIterateしながらハッシュ値を計算します。
    linearSeqHashとは一体何者なのか……

    なので、例えばCompositeな構造のオブジェクトのハッシュ値を取得しようとすると、再帰的にハッシュ値を求めていくことになります。
    末尾再帰最適化がされたりするわけでないと思うので、ちょっと注意が必要かなぁという感じですね。

    プリミティブな場合

    case class A(i: Int, s: String)
    

    な感じだとこうなります。

    override <synthetic> def hashCode(): Int = {
      <synthetic> var acc: Int = -889275714;
      acc = Statics.this.mix(acc, i);
      acc = Statics.this.mix(acc, Statics.this.anyHash(s));
      Statics.this.finalizeHash(acc, 2)
    };
    

    同じProductですが、フィールドの種類に応じて生成されるコードが変わるという点もポイントですね。

    余談

    hashCode##

    どちらもハッシュ値を算出しますが、厳密には挙動が異なります。
    ##APIリファレンスを見ると、次のように記載があります。
    (ボクシングされた)数値型とnull以外はhashCodeと同じだけれど、数値型はその値自身と同じ値をハッシュ値として、nullは0になります。

    Equivalent to x.hashCode except for boxed numeric types and null. For numerics, it returns a hash value which is consistent with value equality: if two value type instances compare as true, then ## will produce the same hash value for each of them. For null returns a hashcode where null.hashCode throws a NullPointerException.

    ちなみにですが、Stack Overflowのこの回答ではLongのhashCode##は異なりますが、2.11.8では同じ値が返るようになっています。Doubleの方は同じ挙動のままでした。