skip to content

Why are properties declared in a data class body (rather than the primary constructor) excluded from generated `equals`/`hashCode`/`toString`, and what surprising bug can this cause?

level: middleimportance: should knowfreq 65%

answer

  1. Only constructor props counted
  2. Body prop = deliberate exclusion
  3. HashSet/HashMap silently drops 'duplicates'
  4. Put identity state in constructor
  5. Mutating a key's hash also breaks collections

basics

~20 s

The compiler only looks at the primary-constructor properties when writing equals, hashCode, and toString. A field added in the body is ignored, so two objects differing only by that field count as equal — which can silently lose data in sets or maps.

solid answer

~40 s

Kotlin generates `equals`/`hashCode`/`toString` strictly from the primary-constructor properties — that's the defined behavior, not an accident. A property declared in the class body (with `var`/`val` after the constructor) is invisible to those members. The classic bug: two instances that differ only in a body property are `equals`, hash the same, and print the same. In a `HashSet` or as a `HashMap` key, the second instance is treated as a duplicate and dropped or overwrites the first, even though it carries different state. The fix is to put any state that should participate in identity into the primary constructor; keep body properties for derived/transient state you intentionally want excluded.

code

kotlin · 8 lines
kotlin
data class CacheKey(val url: String) {
    var hits: Int = 0      // excluded from equals/hashCode
}

val k1 = CacheKey("/a").apply { hits = 5 }
val k2 = CacheKey("/a").apply { hits = 99 }
println(k1 == k2)          // true
println(setOf(k1, k2).size) // 1

go deeper

for a junior

Knows body properties are excluded from the generated members.

for a middle

Explains the HashSet/HashMap silent-duplicate bug and how to fix it by moving state to the constructor.

for a senior

Discusses intentional exclusion for derived/transient state and the var-key mutation hazard for hashCode.

for a principal

Weighs identity modeling trade-offs and team conventions for what belongs in the constructor vs. body across a domain model.

## The rule Kotlin's compiler builds `equals`, `hashCode`, and `toString` for a `data class` from **exactly** the properties listed in the **primary constructor**. Properties declared in the class body are excluded by design — this gives you a deliberate escape hatch for state that shouldn't affect equality (caches, timestamps, lazily computed values). ## Why it's defined this way The primary constructor is the canonical 'identity' of a data object. Body properties often hold derived or mutable bookkeeping that you would not want to change what 'equal' means. So the language draws the line at the constructor. ## The surprising bug Because `equals`/`hashCode` ignore body state, two objects that differ only there are interchangeable to hash-based collections. ```kotlin data class Event(val id: String) { var payload: String = "" // body property -> excluded } val a = Event("e1").apply { payload = "A" } val b = Event("e1").apply { payload = "B" } println(a == b) // true (only id compared) val set = hashSetOf(a) println(set.add(b)) // false -> b rejected as duplicate, payload "B" lost val map = hashMapOf(a to 1) map[b] = 2 println(map.size) // 1 -> b overwrote a's entry ``` The payload difference vanishes in any `HashSet`/`HashMap`/`distinct()` operation. ## How to choose - **Should affect equality** (true identity/value state) -> put it in the **primary constructor**. - **Should NOT affect equality** (cache, lastAccess, lazy field) -> declare it in the **body**, exploiting the exclusion on purpose. ## Note on `hashCode` and mutability Even constructor properties can break hash-based collections if they're `var` and mutated after insertion, because the hash changes while stored. Prefer `val` constructor properties for keys.

  • How do you make a body property participate in equality?
    You can't keep it in the body; move it into the primary constructor. Body properties are always excluded from generated members.
  • Is excluding body properties ever desirable?
    Yes — for transient/derived state like caches, computed flags, or timestamps you don't want to affect equality or the printed form.

The body property is a secret note in your pocket: the bouncer (equals) only checks the name on your ticket (constructor), so two people with the same ticket look identical at the door.

saying these in an interview costs you the question

  • Says it's a bug/limitation rather than intentional, escapable design
  • Suggests overriding equals manually for a body field without understanding hashCode consistency
  • Unaware that hash-based collections silently drop 'equal' duplicates
  • Recommends var constructor properties for map keys without noting the mutation hazard

context