Reusable solution: to have an easy way to select which fields to include in equals()
and hashCode()
, I wrote a little helper called "stem" (essential core data, relevant for equality).
Usage is straightforward, and the resulting code very small:
class Person(val id: String, val name: String) {
private val stem = Stem(this, { id })
override fun equals(other: Any?) = stem.eq(other)
override fun hashCode() = stem.hc()
}
It's possible to trade off the backing field stored in the class with extra computation on-the-fly:
private val stem get() = Stem(this, { id })
Since Stem
takes any function, you are free to specify how the equality is computed. For more than one field to consider, just add one lambda expression per field (varargs):
private val stem = Stem(this, { id }, { name })
Implementation:
class Stem<T : Any>(
private val thisObj: T,
private vararg val properties: T.() -> Any?
) {
fun eq(other: Any?): Boolean {
if (thisObj === other)
return true
if (thisObj.javaClass != other?.javaClass)
return false
// cast is safe, because this is T and other's class was checked for equality with T
@Suppress("UNCHECKED_CAST")
other as T
return properties.all { thisObj.it() == other.it() }
}
fun hc(): Int {
// Fast implementation without collection copies, based on java.util.Arrays.hashCode()
var result = 1
for (element in properties) {
val value = thisObj.element()
result = 31 * result + (value?.hashCode() ?: 0)
}
return result
}
@Deprecated("Not accessible; use eq()", ReplaceWith("this.eq(other)"), DeprecationLevel.ERROR)
override fun equals(other: Any?): Boolean =
throw UnsupportedOperationException("Stem.equals() not supported; call eq() instead")
@Deprecated("Not accessible; use hc()", ReplaceWith("this.hc(other)"), DeprecationLevel.ERROR)
override fun hashCode(): Int =
throw UnsupportedOperationException("Stem.hashCode() not supported; call hc() instead")
}
In case you're wondering about the last two methods, their presence makes the following erroneous code fail at compile time:
override fun equals(other: Any?) = stem.equals(other)
override fun hashCode() = stem.hashCode()
The exception is merely a fallback if those methods are invoked implicitly or through reflection; can be argued if it's necessary.
Of course, the Stem
class could be further extended to include automatic generation of toString()
etc.