skip to content

How do `componentN()` and `copy()` decide which properties they cover, and what happens to properties declared in the class body?

level: middleimportance: should knowfreq 45%

answer

  1. Only constructor properties count
  2. copy() = one param per ctor property
  3. Body properties excluded from all generated members
  4. copy() resets body properties to initializer
  5. Move identity state into the constructor

basics

~10 s

Only the properties listed in the primary constructor are covered. copy() has a parameter per constructor property, and componentN() exposes them in order. Properties declared in the class body are ignored by both.

solid answer

~40 s

The generated `componentN()` functions and `copy()` operate **only** on the primary-constructor properties (those declared `val`/`var` inside the constructor parentheses), in declaration order. `copy()` is generated with one parameter per constructor property, each defaulting to the current value, so `copy()` reconstructs an instance from constructor properties alone. Properties declared in the **class body** are completely excluded: they are not destructured by `componentN`, not copied by `copy()` (they reset to their initializer/default in the new instance), and also excluded from `equals`/`hashCode`/`toString`. This is a frequent bug source: mutating a body property and then calling `copy()` silently loses that mutation. The fix is to make any state that must round-trip a primary-constructor property.

code

kotlin · 10 lines
kotlin
data class Order(val id: Int, val total: Int) {
    var notes: String = ""     // body property
}

val o = Order(1, 100).apply { notes = "rush" }
val c = o.copy(total = 120)
println(c.notes)   // "" — body property NOT copied
println(o == Order(1, 100))        // true — notes ignored

val (id, total) = o   // OK; no component for notes

go deeper

for a junior

Knows copy() and destructuring use the constructor properties and that body properties are different.

for a middle

Explains copy() generates a parameter per constructor property and that body properties reset on copy.

for a senior

Predicts the silent-data-loss bug, explains equals/hashCode exclusion, and advises where to place state.

for a principal

Designs the value boundary deliberately — constructor for identity, body for derived/transient state — and reasons about immutability and caching trade-offs.

## Constructor properties vs body properties Kotlin distinguishes two kinds of properties in a data class: - **Primary-constructor properties** — declared with `val`/`var` *inside* the constructor parentheses. - **Body properties** — declared with `val`/`var` *inside the class body* `{ ... }`. All of the generated members — `equals`, `hashCode`, `toString`, `componentN`, and `copy` — consider **only the primary-constructor properties**, in their declaration order. ## componentN() For a data class with constructor properties `p1, p2, ..., pn`, the compiler generates `component1()` returning `p1`, `component2()` returning `p2`, etc. This is what enables destructuring: ```kotlin data class User(val id: Int, val name: String) { val createdAt = System.nanoTime() } val (id, name) = User(1, "Ann") // createdAt is NOT destructurable ``` There is no `component3()` for `createdAt` because it is a body property. ## copy() `copy()` is generated with one parameter per constructor property, each defaulting to the current value: ```kotlin // conceptually generated: fun copy(id: Int = this.id, name: String = this.name) = User(id, name) ``` Because `copy()` calls the constructor with only constructor properties, **body properties are NOT carried over** — they are re-initialized from their initializer in the copy. ## The classic pitfall ```kotlin data class Doc(val title: String) { var dirty: Boolean = false } val d = Doc("a").apply { dirty = true } val e = d.copy(title = "b") println(e.dirty) // false! body property was reset println(d == Doc("a").apply { dirty = true }) // true: dirty ignored in equals ``` `dirty` is invisible to `equals`, `hashCode`, `toString`, `copy`, and `componentN`. ## Why The generated members are derived purely from the constructor signature — that's the only set of properties the compiler treats as the class's 'identity/value'. Body properties are considered incidental/derived state. ## Practical guidance - Put any field that should participate in equality, copying, or destructuring **in the primary constructor**. - Use body properties only for truly derived or transient state (caches, lazily computed values via `by lazy`). - If you want a derived value that *isn't* part of identity, a body `val` with an initializer is exactly right.

  • Can you destructure a body property of a data class?
    No — componentN is only generated for primary-constructor properties; a body property has no componentN function.
  • How do you keep a derived value cached without breaking copy semantics?
    Declare it in the body, often with `by lazy`, knowing it recomputes on the copied instance; keep it out of identity by design.

The primary constructor is the official passport for the data class — only what's printed there travels through copy() and destructuring; body properties are sticky notes that fall off.

saying these in an interview costs you the question

  • Assuming copy() carries over body properties
  • Expecting destructuring to reach body properties
  • Thinking body properties affect equals/hashCode
  • Storing identity-relevant state in the body
  • Not warning about silent data loss on copy()

context