Data Class With Array Property Missing Overrides
ID |
kotlin.equals_method_overridden_data_classes |
Severity |
high |
Remediation Complexity |
medium |
Remediation Risk |
low |
Remediation Effort |
medium |
Resource |
Code Smell |
Language |
Kotlin |
Tags |
best-practice, reliability |
Description
Reports a data class with an array-typed property in its primary constructor — the generic
Array<T> or any specialised primitive array such as ByteArray or IntArray — that does not
override equals, hashCode and toString.
Only primary-constructor properties count, because those are exactly the properties the compiler
puts into the generated members. A property declared in the class body takes no part in the
generated equals, so an array there is not this defect. A property whose type merely contains an
array (List<ByteArray>) is not itself an array and is not considered.
Rationale
A data class generates equals, hashCode and toString from its primary-constructor
properties — and for an array property those generated members fall back to identity, because
Array.equals is reference equality. Two instances holding arrays with the same elements compare
unequal, hashCode() differs between otherwise-identical instances, and toString() prints
something like [B@1a2b3c instead of the elements.
// FLAW -- payload is compared, hashed and printed by reference
data class Measurement(val subject: String, val payload: ByteArray)
val a = Measurement("temp", byteArrayOf(1, 2))
val b = Measurement("temp", byteArrayOf(1, 2))
a == b // false
a.hashCode() == b.hashCode() // false
println(a) // Measurement(subject=temp, payload=[B@1b6d3586)
This bites hardest where a data class is used as a map key, put in a Set, or compared in a test
assertion — all three of which a data class is otherwise the natural choice for.
Remediation
Override all three members and delegate to the content-based array operations
(contentEquals, contentHashCode, contentToString; the contentDeep* variants for nested
arrays):
data class Measurement(val subject: String, val payload: ByteArray) {
override fun equals(other: Any?): Boolean {
if (other !is Measurement) {
return false
}
return subject == other.subject && payload.contentEquals(other.payload)
}
override fun hashCode(): Int {
return 31 * subject.hashCode() + payload.contentHashCode()
}
override fun toString(): String {
return "Measurement(subject=$subject, payload=${payload.contentToString()})"
}
}
Where the property does not have to be an array, the simpler fix is to change its type: a
List<Byte> compares, hashes and prints by content, so the generated members are then correct as
they stand.