data class User(val name: String, val age: Int) generates, purely from those primary-constructor properties: a value-based equals()/hashCode() pair, a readable toString() (User(name=Alice, age=30)), a copy() function for creating a new instance with select properties changed, and component1()/component2() functions enabling destructuring into separate variables. Crucially, only properties declared IN the primary constructor participate in this generated behavior; a property added in the class body is excluded from equals, hashCode, toString, and copy entirely.
One line replaces dozens
equals compares by VALUE (not reference), and toString produces something actually readable in logs or debugger output — both generated automatically, with zero manual implementation.
data class User(val name: String, val age: Int)
val u1 = User("Alice", 30)
val u2 = User("Alice", 30)
println(u1) // User(name=Alice, age=30) — readable, auto-generated
println(u1 == u2) // true — compares by VALUE, not referencecopy(): a modified duplicate, without mutation
copy() creates a NEW instance, changing only the named parameters and keeping everything else identical — idiomatic for updating an immutable data class without hand-writing a new constructor call listing every property.
val original = User("Alice", 30)
val older = original.copy(age = 31) // NEW instance — original is untouched
println(original) // User(name=Alice, age=30)
println(older) // User(name=Alice, age=31)Destructuring: unpacking into separate variables
This works because data classes automatically generate component1(), component2(), etc. — the destructuring syntax is really calling those functions in order behind the scenes.
val user = User("Alice", 30)
val (name, age) = user // destructures into two separate variables
println("$name is $age") // "Alice is 30"