Phase 3 · Object-Oriented KotlinModule 15~32 min read

Data Classes

Boilerplate-free data holders with generated equals, hashCode, toString, and copy().

What you'll learn

So much of programming is passing around little bundles of data — a point, a user, an API response. A data class gives you all the plumbing for these — equality, printing, copying, destructuring — from a single line. It's one of Kotlin's most-used features.

By the end you'll be able to:

  • Recognise the boilerplate a data class eliminates
  • Declare data classes and use their generated methods
  • Create modified copies with copy()
  • Destructure objects into variables

The boilerplate problem

A plain class that just holds data isn't very useful out of the box: printing it shows a memory address, and two instances with identical contents aren't "equal." Fixing that by hand means writing a lot of repetitive code. A data class does it all for you:

30 lines vs 1 line

A regular class

To be useful, you'd hand-write equals, hashCode, toString, and a copy method — easily 30+ lines of boilerplate.

A data class

data class Point(val x: Int, val y: Int)

One line. Kotlin generates all of it for you.

data class

Add the data keyword and Kotlin generates, from the primary-constructor properties: equals()/hashCode() (compare by value), a readable toString(), copy(), and destructuring support:

Point.kt
data class Point(val x: Int, val y: Int)

val p = Point(3, 4)
println(p)                    // Point(x=3, y=4)  — auto toString
println(p == Point(3, 4))     // true             — auto equals (by value)
println(p.hashCode() == Point(3, 4).hashCode())  // true

copy()

Data classes are usually made of vals (immutable), which is safest. So how do you "change" one? You don't — you make a copy with the changed fields, using named arguments. The original stays untouched:

Copy.kt
data class Person(val name: String, val age: Int)

val sara = Person("Sara", 25)
val older = sara.copy(age = 26)   // a copy with one field changed

println(sara)     // Person(name=Sara, age=25)  — original unchanged
println(older)    // Person(name=Sara, age=26)

Tip

copy() plus immutable vals is the backbone of safe state management (and the basis of how UI state is handled in Jetpack Compose). Model your data as immutable data classes and evolve it with copy().

Destructuring

Because data classes generate componentN() functions, you can destructure them — unpacking their properties into separate variables in one line. This shines in loops and with functions that return multiple values:

Destructure.kt
data class Point(val x: Int, val y: Int)

val (x, y) = Point(3, 4)      // destructure into two variables
println("x=$x, y=$y")

// works beautifully in loops
val points = listOf(Point(1, 2), Point(3, 4))
for ((px, py) in points) {
    println("($px, $py)")
}

Recap & quick check

Key takeaways

  • A data class generates equals, hashCode, toString, copy, and componentN from its constructor properties.
  • equals compares by value, so two data objects with the same contents are equal.
  • Make immutable data classes (vals) and 'change' them with copy(field = newValue).
  • Destructuring unpacks a data object's properties into variables: val (x, y) = point.
  • Only primary-constructor properties are used for the generated methods.

Quick check

1. What does the 'data' keyword generate?

2. How do two data-class instances with the same contents compare with ==?

3. How do you 'change' a field of an immutable data object?

4. What does destructuring let you do?

5. Which properties are used for a data class's generated methods?

Excellent — data classes make modelling data a breeze. Next up: Module 16 — Inheritance & Interfaces.