case classを使うとequals
やhashCode
が自動的に実装されます。
参考:ひしだまさんの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の方は同じ挙動のままでした。