skip to content

Why might copy() silently fail to carry over a property, and what is the relationship between copy() and the primary constructor / equals / hashCode?

level: middleimportance: should knowfreq 55%

answer

  1. Only primary-constructor props feed copy/equals/hashCode
  2. Body properties dropped or reset on copy()
  3. Equal instances can differ in body state
  4. Promote state to constructor with defaults
  5. Body props should be derived (get())

basics

~20 s

copy() only knows about properties listed in the main constructor. A property set inside the class body is recreated from scratch on copy and may be lost or reset. The same constructor-only rule applies to equals, hashCode, and toString.

solid answer

~50 s

The generated copy(), equals(), hashCode(), toString(), and componentN() all consider only the primary-constructor properties. A property declared in the class body (not the constructor) is excluded: copy() has no parameter for it, so the new instance runs the body initializer afresh and any imperatively assigned state is dropped or recomputed. This bites when people store derived or injected state in the body expecting it to travel with copies. It also means two instances can be equal while differing in a body property. The fix: put state that must participate in copy/equals into the primary constructor (using default values for optional ones), and treat body properties strictly as derived (val computed from constructor inputs). If a body field must persist across copies, reconsider the design — data classes are not meant to hold hidden mutable state.

go deeper

for a junior

Learns that copy() parameters come from the primary constructor and body props are not included.

for a middle

Explains the dropped/reset body-state pitfall and the equals/hashCode consistency consequence.

for a senior

Designs data classes so all identity-relevant state is constructor-based with defaults; body is derived only.

for a principal

Sets conventions preventing hidden state in data classes and reasons about equality contracts across the system.

## The rule For a data class, the compiler generates `copy()`, `equals()`, `hashCode()`, `toString()`, and `componentN()` based **only on the properties declared in the primary constructor**. Properties declared in the **class body** are ignored by all of them. ```kotlin data class Account(val id: String) { var balance: Int = 0 // body property, NOT in primary constructor } val a = Account("acc-1").apply { balance = 100 } val b = a.copy() // copy() has no 'balance' parameter println(b.balance) // 0 — re-initialized, the 100 is lost println(a == b) // true — equals ignores 'balance' ``` ## Why this happens `copy()`'s parameter list is generated from the primary constructor. Since `balance` is not a constructor parameter, `copy()` cannot accept or forward it. The new object is constructed via the primary constructor and then the body runs its initializer (`balance = 0`), so the previously assigned value is gone. Likewise, `equals()`/`hashCode()` compare only `id`, so `a` and `b` compare equal despite different balances — a subtle correctness trap if you rely on equality. ## Correct design Put anything that must participate in copy/equality into the **primary constructor**, using **default values** for optional fields: ```kotlin data class Account(val id: String, val balance: Int = 0) val a = Account("acc-1", balance = 100) val b = a.copy() // balance carried over -> 100 ``` Reserve body properties for genuinely **derived** values computed from constructor inputs: ```kotlin data class Circle(val radius: Double) { val area: Double get() = Math.PI * radius * radius // computed, no stored state } ``` A `get()`-only property has no backing state, so there is nothing for copy() to lose. ## Takeaways - copy/equals/hashCode/toString/componentN = primary-constructor properties only. - Body-stored mutable state is dropped/reset by copy() and ignored by equals(). - Promote meaningful state to the constructor (with defaults); keep body props derived.

  • Can two data class instances be equal yet behave differently?
    Yes — if differing state lives in a body property, equals() ignores it, so equal instances can carry different body state.
  • How do you make an optional field participate in copy() without forcing callers to pass it?
    Declare it in the primary constructor with a default value, e.g. val balance: Int = 0.

saying these in an interview costs you the question

  • Storing mutable state in the body and expecting copy() to preserve it
  • Assuming equals() considers body properties
  • Not knowing copy()'s parameters come from the primary constructor
  • Relying on body property values surviving a copy()

context