LoginSignup
2
1

More than 5 years have passed since last update.

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

Posted at

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の方は同じ挙動のままでした。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1