skip to content

Data Classes

What the data modifier generates for you, what you get from it — destructuring, copy, value equality — and the rules it imposes. Data classes are so central to idiomatic Kotlin that almost every interview touches them.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

explore

questions

21

What is a destructuring declaration in Kotlin, and what makes `val (a, b) = point` work under the hood?

level: juniorimportance: must knowfreq 70%

answer

  1. Parentheses on the left = componentN() calls, not a tuple
  2. component1() starts at 1, positional
  3. data class auto-generates for primary-ctor props
  4. operator keyword required
  5. for ((k,v) in map) uses Map.Entry components

basics

~10 s

It lets you pull several values out of one object in a single line, like val (x, y) = point. Kotlin does it by calling special functions named component1(), component2(), and so on.

solid answer

~40 s

A destructuring declaration unpacks an object into multiple variables at once: `val (x, y) = point`. The compiler rewrites it into separate `val` declarations that call positional operator functions: `val x = point.component1()` and `val y = point.component2()`. These `componentN()` functions are conventions marked with the `operator` keyword. `data class` generates them automatically for its primary-constructor properties, in declaration order. Order matters, not names — `val (lat, lng) = point` still binds component1() to `lat`. Any class (even non-data) can support destructuring by declaring `operator fun componentN()` manually. Common uses include iterating a `Map` (`for ((k, v) in map)`) and lambda parameters.

code

kotlin · 10 lines
kotlin
data class Point(val x: Int, val y: Int)

fun main() {
    val (x, y) = Point(3, 7)
    println("$x, $y") // 3, 7
    // compiler-equivalent:
    val p = Point(3, 7)
    val x2 = p.component1()
    val y2 = p.component2()
}

go deeper

for a junior

Knows the syntax val (a, b) = obj and that data classes support it out of the box.

for a middle

Explains the componentN() rewrite, the operator keyword, and that binding is positional.

for a senior

Notes only primary-constructor props get generated components, and that any type can opt in manually.

for a principal

Discusses positional-binding fragility as an API-design hazard and when to avoid exposing destructuring publicly.

## What it is A **destructuring declaration** assigns several variables from a single object in one statement: ```kotlin data class Point(val x: Int, val y: Int) val point = Point(3, 7) val (x, y) = point // x = 3, y = 7 ``` ## How the compiler expands it The parentheses are NOT a tuple. The compiler translates the declaration into one ordinary `val` per name, each calling a positional **operator function**: ```kotlin val x = point.component1() val y = point.component2() ``` These `componentN()` functions must be marked with the `operator` keyword and follow the naming convention `component1`, `component2`, … starting at 1. ## Where they come from - For a `data class`, the compiler **auto-generates** `componentN()` for each property declared in the **primary constructor**, in declaration order. Properties added in the class body do NOT get one. - For any other type you can write them by hand: ```kotlin class Pair2(val a: Int, val b: Int) { operator fun component1() = a operator fun component2() = b } ``` ## Position, not name Binding is purely positional. `val (lat, lng) = point` binds `lat` to `component1()` and `lng` to `component2()` regardless of the names. Choosing the wrong order silently gives wrong values — a real bug source. ## Common contexts - Maps: `for ((key, value) in map) { ... }` works because `Map.Entry` defines `component1()`/`component2()` as extensions. - Lambdas: `list.map { (a, b) -> a + b }` destructures each element parameter. - Multiple return: return a `data class` (or `Pair`/`Triple`) and destructure at the call site. Destructuring is a compile-time convention, costs nothing extra beyond the function calls, and is unrelated to pattern matching in other languages.

  • Does destructuring match by variable name or by position?
    By position. The first name gets component1(), the second component2(), etc. Names are arbitrary; only order matters.
  • Can a non-data class be destructured?
    Yes — declare `operator fun componentN()` manually. data class just generates them for you.

Like unpacking a labeled box by slot number, not by label name — slot 1 always goes to the first variable.

saying these in an interview costs you the question

  • Thinking `val (a, b)` creates a tuple object
  • Believing destructuring matches property names
  • Claiming only data classes can ever be destructured
  • Saying componentN() starts at 0

context

open as a page

What does the generated copy() function on a Kotlin data class do, and why is it useful for immutable data?

level: juniorimportance: must knowfreq 80%

basics

~10 s

copy() makes a new object that is the same as the old one, except for the properties you choose to change. The original stays untouched, so you update data without modifying it in place.

open as a page

What members does the Kotlin compiler auto-generate when you mark a class as a `data class`, and what drives their content?

level: juniorimportance: must knowfreq 85%

basics

~10 s

Marking a class data makes the compiler write equals(), hashCode(), and toString() for you, based on the properties declared in the primary constructor, so you don't write that boilerplate by hand.

open as a page

What are the requirements on a data class's primary constructor, and why won't `data class Empty()` compile?

level: juniorimportance: must knowfreq 70%

basics

~20 s

A data class must declare a primary constructor with at least one parameter, and every parameter must be marked val or var. An empty parameter list, or a plain parameter without val/var, is a compile error.

open as a page

Exactly which properties of a data class get a generated componentN(), and what gotchas does that create for destructuring?

level: middleimportance: must knowfreq 50%

basics

~10 s

Only the properties listed in the data class's main constructor get componentN() functions, in the order they're written. Properties declared inside the class body don't get one, so you can't destructure them.

open as a page

copy() performs a shallow copy. What problems can this cause when a data class holds mutable or nested objects, and how do you update nested immutable state correctly?

level: middleimportance: must knowfreq 70%

basics

~20 s

copy() only copies the top object. Anything it points to (lists, other objects) is shared with the original. If that shared thing is mutable, changing it affects both. To update nested data, copy() the inner object too and slot it in.

open as a page

Which class modifiers are forbidden on a data class (abstract, open, sealed, inner), and why?

level: middleimportance: must knowfreq 55%

basics

~10 s

A data class cannot be abstract, open, sealed, or inner. It is implicitly final and standalone. You can still make it implement interfaces or be a member of (but not inner to) another class.

open as a page

How do you skip a component you don't need in a destructuring declaration, and what does the `_` placeholder actually do?

level: juniorimportance: should knowfreq 55%

basics

~10 s

Put an underscore _ in the position you want to ignore: val (_, y) = point. Kotlin then skips that value and doesn't create a variable for it.

open as a page

How do you make a class that is not a data class destructurable, and what are the exact rules for componentN() operator functions?

level: middleimportance: should knowfreq 45%

basics

~10 s

Add functions named component1(), component2(), and so on, each marked with the operator keyword. They can also be extension functions, so you can even add destructuring to types you don't own.

open as a page

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%

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.

open as a page

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%

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.

open as a page

What risks does the auto-generated `toString()` of a data class introduce in production logging, and how do you mitigate them?

level: middleimportance: should knowfreq 45%

basics

~10 s

The generated toString prints every constructor property, including secrets like passwords or tokens. If you log a data object, those values can leak into logs. You should override toString to mask sensitive fields.

open as a page

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%

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.

open as a page

Explain how destructuring interacts with `for` loops, lambda parameters, and the stdlib collection APIs. Where do the componentN() functions come from in each case?

level: seniorimportance: should knowfreq 40%

basics

~20 s

You can unpack each element of a loop or each lambda argument using parentheses, e.g. for ((k, v) in map) or list.map { (a, b) -> ... }. It works because the element type provides component1(), component2(), etc.

open as a page

Show how copy() underpins immutable state transitions in a reducer-style update. Why is this preferable to in-place mutation, especially for concurrency and frameworks like Jetpack Compose / StateFlow?

level: seniorimportance: should knowfreq 60%

basics

~20 s

You model state as an immutable data class and produce each new state with copy(). Because every state is a distinct, unchanging object, comparing old vs new is reliable, change detection works, and concurrent readers never see a half-updated object.

open as a page

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%

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.

open as a page

If a data class extends a superclass that defines `toString()`/`equals()`, or you write your own `toString()` in the data class, which one wins — the generated or the explicit one? How does inheritance interact with the generated members?

level: seniorimportance: nice to knowfreq 35%

basics

~10 s

If you write your own equals, hashCode, or toString inside the data class, the compiler uses yours instead of generating that one. The compiler only fills in the members you didn't provide.

open as a page

What happens to `copy()` and `componentN()` if a data class's primary-constructor property is private, and what visibility pitfalls follow?

level: seniorimportance: nice to knowfreq 25%

basics

~10 s

The generated copy() and componentN() still cover every constructor property, including private ones, and copy() itself is public. So a private property can leak out through copy(), which can undermine your intended encapsulation.

open as a page

From an API-design and maintenance standpoint, when should you avoid relying on componentN()/destructuring, and what concrete failure modes does positional binding introduce?

level: principalimportance: nice to knowfreq 25%

basics

~10 s

Avoid destructuring when the order of fields isn't obvious or might change. Because values are matched by position, swapping two same-typed fields silently gives wrong results with no error, which is hard to catch.

open as a page

What are the design limitations of relying on data class copy() for immutable updates at scale, and what alternatives or mitigations would you consider?

level: principalimportance: nice to knowfreq 35%

basics

~20 s

copy() is great for small, flat objects but gets painful for deep or large state: lots of nested copy() calls, only-public-constructor updates, shallow copying, and allocation cost. For big trees, use lens libraries, restructure state, or normalize it.

open as a page

When modeling immutable value types, when do data class constraints push you toward a non-data class, a value class, or a @JvmRecord, and what constraints differ?

level: principalimportance: nice to knowfreq 15%

basics

~20 s

Data classes can't be open/abstract/sealed/inner and always generate copy(), which leaks private state and boxes single fields. For invariants use a plain class plus factory; for one wrapped value use @JvmInline value class; for Java record interop use @JvmRecord on the data class.

open as a page