skip to content

Explain the practical and performance differences between declaring a primary-constructor parameter as a property (val/var) versus a plain parameter.

level: seniorimportance: should knowfreq 50%

answer

  1. val/var → lifetime field + getter
  2. plain param → construction-time only
  3. Captured plain param gets stored
  4. data class: all params must be val/var
  5. Plain param hides input from API

basics

~20 s

A val/var parameter is stored as a field for the object's whole life and is readable later. A plain parameter is only available while the object is being built, so it is not kept around.

solid answer

~40 s

Marking a constructor parameter `val`/`var` allocates a **backing field** that lives for the object's lifetime and exposes a getter (and setter for `var`). A plain parameter exists only during construction: it can be read by property initializers and `init` blocks but is **not** retained as a field unless something captures it. Capturing matters: if a plain parameter is referenced from a lambda, a nested/anonymous object, or used to initialize a property, the compiler keeps whatever is needed. Practically: use plain parameters for values you only transform during init (avoiding a redundant field), and `val` properties for values you must read later. For `data class`, only `val`/`var` parameters participate in `equals`/`hashCode`/`toString`/`copy` — plain parameters are not even allowed in the data-class primary constructor.

code

kotlin · 8 lines
kotlin
class Order(val id: Long, rawTotal: Double) {
    // rawTotal not kept as a field; only the rounded property is
    val total: Double = Math.round(rawTotal * 100) / 100.0
}

val o = Order(1, 9.999)
println(o.total)  // 10.0
// o.rawTotal     // compile error: not a member

go deeper

for a junior

Knows val/var makes a property and plain doesn't.

for a middle

Knows plain params are construction-time only and val/var define the API surface.

for a senior

Explains backing-field lifetime, capture-driven storage, and the data-class requirement.

for a principal

Reasons about encapsulation, memory footprint per instance, and immutability defaults across a large model layer.

## Two kinds of header parameter ```kotlin class A(val kept: Int, transient: Int) { val derived = transient * 2 // transient used here, then gone } ``` - `kept` → a **property** with a backing field that survives for the object's lifetime, plus a generated getter. - `transient` → a **construction-time-only** parameter, visible to initializers and `init` blocks. ## Memory / field generation - Each `val`/`var` parameter normally produces a **backing field** on the object → real memory per instance. - A plain parameter produces **no field by itself**. If you never capture it, it lives only on the stack during the constructor call. - **Capture rule:** if a plain parameter is referenced from something that outlives the constructor — an inner/anonymous `object`, a lambda stored in a property, or a property initializer that retains it — the compiler synthesizes storage to keep it alive. So "no field" holds only when there is no such capture. ```kotlin class Counter(start: Int) { private val increment = { start + 1 } // captures start -> stored } ``` ## API surface - `val`/`var` becomes part of the **public (or chosen-visibility) API**: callers can read `obj.kept`. - A plain parameter is **invisible** outside; it cannot be read as a member. This is good for encapsulation when the raw input should not be exposed. ## data class interaction For a `data class`, **every** primary-constructor parameter **must** be `val`/`var`; plain parameters are a compile error there. Only these property parameters feed generated `equals`, `hashCode`, `toString`, and `componentN`/`copy`. ## Choosing - Need to read it later, expose it, or include it in data-class equality → `val`/`var`. - Only needed to compute something during init, and you don't want a field or public member → plain parameter. - Want mutation later → `var` (but prefer immutability; reserve `var` for genuine mutable state). ## Subtlety: shadowing vs reuse ```kotlin class B(name: String) { val name = name.trim() // property 'name' shadows the parameter after this line } ``` Here `name` (param) initializes `name` (property); after initialization only the property remains.

  • Does a plain constructor parameter always avoid creating a field?
    Only if it isn't captured. If a lambda, inner/anonymous object, or retaining initializer references it, the compiler generates storage to keep it alive.
  • Why must every parameter in a data class primary constructor be val or var?
    Because data classes generate equals/hashCode/toString/copy from the constructor properties, and plain parameters wouldn't be members to generate from — so the compiler forbids them.

saying these in an interview costs you the question

  • Saying val and plain parameters cost the same memory regardless of capture
  • Claiming plain parameters are accessible as members
  • Not knowing data classes forbid plain primary-constructor params
  • Always reaching for var instead of val
  • Ignoring that capture can force storage for a plain param

context