Skip to content

Commit 0b53939

Browse files
authored
Merge pull request scala/scala#11023 from lrytz/t13033b
Mix in the `productPrefix` hash statically in case class `hashCode`
2 parents 6ba3a2c + a31a5fe commit 0b53939

File tree

2 files changed

+77
-15
lines changed

2 files changed

+77
-15
lines changed

library/src/scala/runtime/ScalaRunTime.scala

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,10 +152,16 @@ object ScalaRunTime {
152152
// More background at ticket #2318.
153153
def ensureAccessible(m: JMethod): JMethod = scala.reflect.ensureAccessible(m)
154154

155+
// This is called by the synthetic case class `toString` method.
156+
// It originally had a `CaseClass` parameter type which was changed to `Product`.
155157
def _toString(x: Product): String =
156158
x.productIterator.mkString(x.productPrefix + "(", ",", ")")
157159

158-
def _hashCode(x: Product): Int = scala.util.hashing.MurmurHash3.productHash(x)
160+
// This method is called by case classes compiled by older Scala 2.13 / Scala 3 versions, so it needs to stay.
161+
// In newer versions, the synthetic case class `hashCode` has either the calculation inlined or calls
162+
// `MurmurHash3.productHash`.
163+
// There used to be an `_equals` method as well which was removed in 5e7e81ab2a.
164+
def _hashCode(x: Product): Int = scala.util.hashing.MurmurHash3.caseClassHash(x)
159165

160166
/** A helper for case classes. */
161167
def typedProductIterator[T](x: Product): Iterator[T] = {

library/src/scala/util/hashing/MurmurHash3.scala

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,15 +60,16 @@ private[hashing] class MurmurHash3 {
6060
finalizeHash(h, 2)
6161
}
6262

63-
/** Compute the hash of a product */
63+
// @deprecated("use `caseClassHash` instead", "2.13.17")
64+
// The deprecation is commented because this method is called by the synthetic case class hashCode.
65+
// In this case, the `seed` already has the case class name mixed in and `ignorePrefix` is set to true.
66+
// Case classes compiled before 2.13.17 call this method with `productSeed` and `ignorePrefix = false`.
67+
// See `productHashCode` in `SyntheticMethods` for details.
6468
final def productHash(x: Product, seed: Int, ignorePrefix: Boolean = false): Int = {
6569
val arr = x.productArity
66-
// Case objects have the hashCode inlined directly into the
67-
// synthetic hashCode method, but this method should still give
68-
// a correct result if passed a case object.
69-
if (arr == 0) {
70-
x.productPrefix.hashCode
71-
} else {
70+
if (arr == 0)
71+
if (!ignorePrefix) x.productPrefix.hashCode else seed
72+
else {
7273
var h = seed
7374
if (!ignorePrefix) h = mix(h, x.productPrefix.hashCode)
7475
var i = 0
@@ -80,6 +81,24 @@ private[hashing] class MurmurHash3 {
8081
}
8182
}
8283

84+
/** See the [[MurmurHash3.caseClassHash(x:Product,caseClassName:String)]] overload */
85+
final def caseClassHash(x: Product, seed: Int, caseClassName: String): Int = {
86+
val arr = x.productArity
87+
val aye = (if (caseClassName != null) caseClassName else x.productPrefix).hashCode
88+
if (arr == 0) aye
89+
else {
90+
var h = seed
91+
h = mix(h, aye)
92+
var i = 0
93+
while (i < arr) {
94+
h = mix(h, x.productElement(i).##)
95+
i += 1
96+
}
97+
finalizeHash(h, arr)
98+
}
99+
}
100+
101+
83102
/** Compute the hash of a string */
84103
final def stringHash(str: String, seed: Int): Int = {
85104
var h = seed
@@ -337,14 +356,46 @@ object MurmurHash3 extends MurmurHash3 {
337356
final val mapSeed = "Map".hashCode
338357
final val setSeed = "Set".hashCode
339358

340-
def arrayHash[@specialized T](a: Array[T]): Int = arrayHash(a, arraySeed)
341-
def bytesHash(data: Array[Byte]): Int = bytesHash(data, arraySeed)
342-
def orderedHash(xs: IterableOnce[Any]): Int = orderedHash(xs, symmetricSeed)
343-
def productHash(x: Product): Int = productHash(x, productSeed)
344-
def stringHash(x: String): Int = stringHash(x, stringSeed)
345-
def unorderedHash(xs: IterableOnce[Any]): Int = unorderedHash(xs, traversableSeed)
359+
def arrayHash[@specialized T](a: Array[T]): Int = arrayHash(a, arraySeed)
360+
def bytesHash(data: Array[Byte]): Int = bytesHash(data, arraySeed)
361+
def orderedHash(xs: IterableOnce[Any]): Int = orderedHash(xs, symmetricSeed)
362+
def stringHash(x: String): Int = stringHash(x, stringSeed)
363+
def unorderedHash(xs: IterableOnce[Any]): Int = unorderedHash(xs, traversableSeed)
346364
def rangeHash(start: Int, step: Int, last: Int): Int = rangeHash(start, step, last, seqSeed)
347365

366+
@deprecated("use `caseClassHash` instead", "2.13.17")
367+
def productHash(x: Product): Int = caseClassHash(x, productSeed, null)
368+
369+
/**
370+
* Compute the `hashCode` of a case class instance. This method returns the same value as `x.hashCode`
371+
* if `x` is an instance of a case class with the default, synthetic `hashCode`.
372+
*
373+
* This method can be used to implement case classes with a cached `hashCode`:
374+
* {{{
375+
* case class C(data: Data) {
376+
* override lazy val hashCode: Int = MurmurHash3.caseClassHash(this)
377+
* }
378+
* }}}
379+
*
380+
* '''NOTE''': For case classes (or subclasses) that override `productPrefix`, the `caseClassName` parameter
381+
* needs to be specified in order to obtain the same result as the synthetic `hashCode`. Otherwise, the value
382+
* is not in sync with the case class `equals` method (scala/bug#13033).
383+
*
384+
* {{{
385+
* scala> case class C(x: Int) { override def productPrefix = "Y" }
386+
*
387+
* scala> C(1).hashCode
388+
* val res0: Int = -668012062
389+
*
390+
* scala> MurmurHash3.caseClassHash(C(1))
391+
* val res1: Int = 1015658380
392+
*
393+
* scala> MurmurHash3.caseClassHash(C(1), "C")
394+
* val res2: Int = -668012062
395+
* }}}
396+
*/
397+
def caseClassHash(x: Product, caseClassName: String = null): Int = caseClassHash(x, productSeed, caseClassName)
398+
348399
private[scala] def arraySeqHash[@specialized T](a: Array[T]): Int = arrayHash(a, seqSeed)
349400
private[scala] def tuple2Hash(x: Any, y: Any): Int = tuple2Hash(x.##, y.##, productSeed)
350401

@@ -397,8 +448,13 @@ object MurmurHash3 extends MurmurHash3 {
397448
def hash(xs: IterableOnce[Any]) = orderedHash(xs)
398449
}
399450

451+
@deprecated("use `caseClassHashing` instead", "2.13.17")
400452
def productHashing = new Hashing[Product] {
401-
def hash(x: Product) = productHash(x)
453+
def hash(x: Product) = caseClassHash(x)
454+
}
455+
456+
def caseClassHashing = new Hashing[Product] {
457+
def hash(x: Product) = caseClassHash(x)
402458
}
403459

404460
def stringHashing = new Hashing[String] {

0 commit comments

Comments
 (0)