skip to content

Explain how a data class's generated `equals` and `hashCode` satisfy the equals/hashCode contract, and what happens if a primary-constructor property is an array.

level: seniorimportance: should knowfreq 50%

answer

  1. Same props -> equal implies equal hash
  2. Array.equals is referential, not content
  3. Use contentEquals/contentHashCode
  4. Or model as List for content equality
  5. val keys to keep hashCode stable

basics

~20 s

The generated equals and hashCode use the same set of constructor properties, so equal objects always produce equal hashes — that's the contract. But arrays are compared by reference, so two data objects holding equal-content arrays can wrongly come out unequal.

solid answer

~30 s

The compiler generates `equals` and `hashCode` from the same primary-constructor properties, guaranteeing the core contract: if `a == b` then `a.hashCode() == b.hashCode()`. `equals` is reflexive, symmetric, transitive, and consistent because it just `&&`-compares each property with `==` and combines hashes deterministically. The trap is reference types whose own `equals` is identity-based — most notably `Array`. `Array.equals` is referential, so `data class Holder(val data: IntArray)` will report two holders with content-equal arrays as unequal, and their hashes differ. The fix is to override `equals`/`hashCode` manually using `contentEquals`/`contentHashCode` (or `contentDeepEquals` for nested arrays), or model the field as a `List` instead.

code

kotlin · 5 lines
kotlin
data class A(val xs: IntArray)
data class B(val xs: List<Int>)

println(A(intArrayOf(1)) == A(intArrayOf(1)))  // false (array identity)
println(B(listOf(1)) == B(listOf(1)))          // true  (List content equality)

go deeper

for a junior

Knows equal objects should have equal hash codes.

for a middle

Can state the full contract and that generated members satisfy it via the shared property set.

for a senior

Identifies the array referential-equality trap and fixes it with contentEquals/contentHashCode or List.

for a principal

Reasons about API/data-modeling choices (List vs array) and consistency guarantees across serialization and collection use.

## The equals/hashCode contract Java/Kotlin's contract requires: 1. **Reflexive**: `a == a` is true. 2. **Symmetric**: `a == b` iff `b == a`. 3. **Transitive**: if `a == b` and `b == c` then `a == c`. 4. **Consistent**: repeated calls give the same result if state is unchanged. 5. **hashCode consistency**: if `a == b` then `a.hashCode() == b.hashCode()`. ## How generated members satisfy it The generated `equals` essentially does: ```kotlin override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is User) return false return name == other.name && age == other.age } ``` and `hashCode` combines the same properties (`31 * name.hashCode() + age`). Because both use the **same property set** with the **same `==`/`hashCode`**, equal objects necessarily hash equally — point 5 holds for free. The boolean `&&` chain makes it reflexive/symmetric/transitive as long as each property's own `equals` obeys the contract. ## The array trap `Array` (and `IntArray`, etc.) defines `equals` as **referential identity**, not content. So: ```kotlin data class Holder(val data: IntArray) val h1 = Holder(intArrayOf(1, 2)) val h2 = Holder(intArrayOf(1, 2)) println(h1 == h2) // false! arrays compared by reference ``` The generated `equals` faithfully calls `data == other.data`, which for arrays is reference comparison — so content-equal holders are unequal, and `hashCode` differs too. The contract is technically still satisfied (it's consistent with array identity), but the behavior surprises everyone. ## Fixes - **Override manually** using content-aware functions: ```kotlin data class Holder(val data: IntArray) { override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is Holder) return false return data.contentEquals(other.data) } override fun hashCode(): Int = data.contentHashCode() } ``` Use `contentDeepEquals`/`contentDeepHashCode` for arrays of arrays. - **Prefer `List`**: `data class Holder(val data: List<Int>)` — `List.equals` is content-based, so generated members 'just work'. ## Mutability caveat `hashCode` consistency assumes the property values don't change while the object lives in a hash structure. Mutating a `var` constructor property after insertion into a `HashSet`/`HashMap` corrupts lookups. Favor `val` for keys.

  • Why doesn't the compiler just use contentEquals automatically for array properties?
    Generated equals uniformly calls each property's own equals; arrays define identity equals, and the compiler doesn't special-case them — it only warns and leaves the override to you.
  • Does Kotlin warn about array properties in data classes?
    Yes — the compiler emits a warning suggesting you override equals/hashCode or use contentEquals when a data class has an array property used in equals.

saying these in an interview costs you the question

  • Believes arrays compare by content in data class equals
  • Cannot state the hashCode-consistency rule
  • Overrides equals but forgets to override hashCode to match
  • Uses contentEquals for nested arrays instead of contentDeepEquals
  • Thinks generated equals violates the contract for arrays (it's consistent, just identity-based)

context