Data Classes
Data classes, generated equals/hashCode/copy, and their use as map keys.
8 questions
JuniorTheoryVery commonWhat members does the compiler generate for a Kotlin data class?
What members does the compiler generate for a Kotlin data class?
For a data class the compiler generates equals, hashCode, toString, copy and the componentN functions. All of them derive from the primary-constructor properties only — a property declared in the class body takes part in none of them.
Common mistakes
- ✗Thinking a body-declared property takes part in the generated
equals/hashCode - ✗Expecting
copyto have a parameter for a property declared in the body - ✗Believing an ordinary class also gets structural
equalsfor free
Follow-up questions
- →Which of the generated members may you still override by hand?
- →What does the generated
copydo with a property declared in the body?
JuniorTheoryCommonWhat makes a type destructurable in val (a, b) = x?
What makes a type destructurable in val (a, b) = x?
Destructuring is not tuple magic: the compiler rewrites val (a, b) = x into x.component1() and x.component2(). Any type is destructurable once it declares those operator fun componentN(); a data class gets them generated from its primary-constructor properties.
Common mistakes
- ✗Thinking destructuring binds by property name rather than by position
- ✗Assuming only a
data classcan ever be destructured - ✗Forgetting that
componentNmust be declaredoperatorto work
Follow-up questions
- →How do you skip an unwanted position inside a destructuring declaration?
- →Why is reordering a
data classprimary constructor a silent break for callers?
JuniorDebuggingCommonWhy does this data class used as a map key behave unexpectedly?
Why does this data class used as a map key behave unexpectedly?
A data class builds equals/hashCode from primary-constructor properties only, excluding the body-declared id. The culprit is Address: a plain class compared by identity, so the two users are unequal. Output 2 then null; fix by making Address a data class.
Common mistakes
- ✗Thinking
data classequality covers body-declared properties likeid - ✗Assuming a nested non-
datafield is compared by value automatically - ✗Believing
mapOfdeduplicates keys differently from aHashMap
Follow-up questions
- →Which properties does the generated
copyfunction take parameters for? - →Why must
hashCodeagree withequalsfor correct map lookups?
MiddleTheoryOccasionalWhy can a data class never be open, and what do you do instead?
Why can a data class never be open, and what do you do instead?
A data class is implicitly final: a subclass adding state would break the generated equals/hashCode/copy, which only ever see the primary-constructor properties. It may still implement interfaces and extend a class — for reuse, prefer composition or a sealed hierarchy.
Common mistakes
- ✗Thinking
openon adata classmerely needs to be spelled out explicitly - ✗Assuming a
data classalso cannot implement interfaces or extend a class - ✗Expecting a subclass property to enter the inherited generated
equals
Follow-up questions
- →How does a sealed hierarchy of data classes replace inheritance from one?
- →Why does interface delegation fit a
data classbetter than a base class would?
MiddleTheoryOccasionalWhen must you write equals/hashCode on a data class by hand?
When must you write equals/hashCode on a data class by hand?
Whenever identity is not exactly the primary-constructor properties. An Array property is compared by reference, so equal contents look unequal; a timestamp field splits objects that are the same thing. The pair you write must stay consistent — equal objects must return the same hashCode.
Common mistakes
- ✗Assuming an
Arrayproperty is compared by content in the generatedequals - ✗Overriding
equalsalone and leaving the generatedhashCodeinconsistent - ✗Putting a timestamp or cache field in the primary constructor of a
data class
Follow-up questions
- →Which stdlib function compares two arrays by content instead of by reference?
- →What breaks in a
HashMapwhenhashCodedisagrees withequals?
MiddleCodeOccasionalFormat a Map by destructuring its entries in the for header
Format a Map by destructuring its entries in the for header
The loop iterates Map.Entry values and destructures each, so k is component1() and v is component2() — stdlib operator extensions on Map.Entry, so the entry need not be a data class. Entries follow the map's own iteration order; an empty map gives an empty string.
Common mistakes
- ✗Thinking
Map.Entryis adata classrather than an interface withcomponentNextensions - ✗Assuming a destructuring
forheader allocates aPairper entry - ✗Expecting the entry order to be undefined for every
Mapimplementation
Follow-up questions
- →How would you destructure only the value and ignore the key in that loop?
- →Which
Mapimplementation guarantees the insertion order you rely on here?
MiddleTheoryOccasionalWhen is Pair/Triple the right choice over a purpose-built data class?
When is Pair/Triple the right choice over a purpose-built data class?
Pair and Triple are generic data classes whose components are named first, second, third — fine for a short-lived local pairing or a private return value. In a public API those names carry no meaning: a purpose-built data class names its fields and can validate them in init.
Common mistakes
- ✗Thinking
Pairlacks structural equality and cannot be a map key - ✗Returning a
Pairfrom a public API where the component names carry no meaning - ✗Believing
Pair/Tripleare allocation-free tuples rather than data classes
Follow-up questions
- →What does
toactually build, and why is it only an infix function? - →How does a named
data classreturn type ease a later change to the result shape?
SeniorTheoryRareHow does Kotlin data class equality differ from a Java record?
How does Kotlin data class equality differ from a Java record?
A record's state is exactly its components, so the generated equals/hashCode cover all of it. A data class may hold extra body-declared properties, and those are excluded — two objects can compare equal while differing in body state, and copy silently drops that state.
Common mistakes
- ✗Assuming a
data classand arecordhave identical equality semantics - ✗Forgetting that a
recordhas no place to put state outside its components - ✗Expecting
copyto carry over a property declared in the class body
Follow-up questions
- →Why can a
recordnot declare an extra instance field outside its components? - →What does the Kotlin compiler emit when you mark a
data class@JvmRecord?