Kotlin Data Classes

Hold data with one line of code using data classes, which generate toString, equals, hashCode, copy and component functions for destructuring.

Lesson 14 of 43Intermediate17 min read

Many classes exist just to hold data: a user with a name and email, a product with a price, a point with x and y, a response from a web API. For these, you usually want the same handful of features: a readable printout, a way to compare two objects by their values, and an easy way to make a slightly changed copy.

Kotlin's data classes give you all of that by adding one word: data. From the properties in the primary constructor, the compiler generates toString(), equals(), hashCode(), copy() and functions for destructuring. A class that would need dozens of lines of boilerplate in many languages becomes a single line.

In this lesson you'll declare data classes, see what the compiler generates, make changed copies instead of modifying objects, unpack objects into variables, and learn when a regular class is the better choice.

Declaring a data class

Kotlin
data class User(val name: String, val email: String, val age: Int)

val u = User("Ada", "ada@example.com", 36)
println(u)          // User(name=Ada, email=ada@example.com, age=36)

The compiler generates, from the constructor properties:

  • toString(), giving the readable format above instead of something like User@6d06d69c.
  • equals() and hashCode(), so two users with the same values are equal.
  • copy(), to create a modified copy.
  • component1(), component2() and so on, which power destructuring.

The primary constructor must have at least one val or var parameter.

Equality and copy

With a regular class, == on two separately created objects is false, even if their values match, because they're different objects. With a data class, == compares the values:

Kotlin
User("Ada", "a@x.com", 36) == User("Ada", "a@x.com", 36)   // true

Data classes are usually written with val properties, so objects never change. To "change" one, make a copy with only the differences:

Kotlin
val older = u.copy(age = 37)

u is untouched, so any code holding it still sees the original values. This style, called immutable data, avoids a whole family of bugs where one part of a program changes an object another part is still using.

Destructuring

A data class can be unpacked into separate variables in the order of its properties:

Kotlin
val (name, email) = u            // takes the first two
for ((name, _, age) in users) { } // _ skips a property

This is the same mechanism that unpacks Pair and map entries. Destructuring is by position, not by name, so val (email, name) = u would put the name into email. Keep destructuring to small, obvious cases.

Data classes in collections

Because equality and hash codes are based on values, data classes work naturally as map keys and set items: a setOf containing two equal users holds just one. Sorting, grouping and searching by properties work well with the collection operations you've already learned.

Only properties in the primary constructor take part in the generated functions. A property declared in the class body is ignored by toString, equals and copy, which can be useful for cached or derived values, but surprising if you forget.

Kotlin Data Classes Syntax

Data class essentials

data class Point(val x: Int, val y: Int)
val p = Point(1, 2)
println(p)                 // Point(x=1, y=2)
p == Point(1, 2)           // true
val q = p.copy(y = 5)
val (x, y) = p
setOf(p, Point(1, 2)).size // 1

Kotlin Data Classes Summary

  • data class generates toString, equals, hashCode, copy and componentN from constructor properties.
  • == compares data class objects by value.
  • copy(prop = value) makes a changed copy; prefer val properties for immutable data.
  • Destructuring val (a, b) = obj unpacks properties by position.
  • Only primary constructor properties are used by the generated functions.

Kotlin Data Classes Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Generated functions

Kotlin
data class Product(val name: String, val price: Double, val stock: Int = 0)
class PlainProduct(val name: String, val price: Double)

fun main() {
    val a = Product("Mug", 8.5, 12)
    val b = Product("Mug", 8.5, 12)
    println(a)
    println(a == b)

    val c = PlainProduct("Mug", 8.5)
    val d = PlainProduct("Mug", 8.5)
    println(c == d)
}
Output
Product(name=Mug, price=8.5, stock=12)
true
false
Line-by-line explanation
  1. Line 1One line gives a full-featured data holder.
  2. Line 7The generated toString lists every property.
  3. Line 8Data classes compare values, so these are equal.
  4. Line 12Regular classes compare identity: two separate objects aren't equal.
Example 2

copy and destructuring

Kotlin
data class Order(val id: Int, val item: String, val qty: Int, val shipped: Boolean = false)

fun main() {
    val order = Order(1042, "Notebook", 3)
    val updated = order.copy(qty = 5)
    val sent = updated.copy(shipped = true)

    println(order)
    println(sent)

    val (id, item, qty) = sent
    println("#$id: $qty x $item")
}
Output
Order(id=1042, item=Notebook, qty=3, shipped=false)
Order(id=1042, item=Notebook, qty=5, shipped=true)
#1042: 5 x Notebook
Line-by-line explanation
  1. Lines 5 to 6Each copy changes only the named properties; earlier versions stay as they were.
  2. Line 8The original order still has qty 3.
  3. Line 11Destructuring takes the first three properties in order.
Example 3

Data classes in sets, maps and sorting

Kotlin
data class Point(val x: Int, val y: Int)
data class Player(val name: String, val score: Int)

fun main() {
    val visited = setOf(Point(0, 0), Point(1, 2), Point(0, 0))
    println(visited.size)

    val labels = mapOf(Point(1, 2) to "Treasure")
    println(labels[Point(1, 2)])

    val players = listOf(Player("Ben", 340), Player("Ana", 120), Player("Cleo", 340))
    val ranking = players.sortedWith(compareByDescending<Player> { it.score }.thenBy { it.name })
    for ((name, score) in ranking) println("$name $score")
}
Output
2
Treasure
Ben 340
Cleo 340
Ana 120
Line-by-line explanation
  1. Lines 5 to 6Two equal points count as one in a set.
  2. Line 9A brand new Point(1, 2) finds the entry, because equal values give equal hash codes.
  3. Line 12Sort by score, highest first, then by name to break ties.
  4. Line 13Destructuring works in for loops too.

Common Kotlin Data Classes Mistakes

Using var and changing shared data

Don't
data class Cart(var items: MutableList<String>)
val saved = cart
cart.items.add("Lamp")          // saved changes too: it's the same object
Do
data class Cart(val items: List<String>)
val newCart = cart.copy(items = cart.items + "Lamp")

Properties in the body are ignored

Don't
data class User(val name: String) {
    var age: Int = 0              // not in toString, equals or copy
}
Do
data class User(val name: String, val age: Int = 0)

Destructuring in the wrong order

Don't
val (email, name) = User("Ada", "ada@example.com", 36)   // email = "Ada"
Do
val (name, email) = User("Ada", "ada@example.com", 36)

Kotlin Data Classes in Real Projects

Data classes in practice

Data classes model almost every piece of data in modern Kotlin apps: JSON from web APIs (parsed with libraries like kotlinx.serialization and Moshi), database rows with Room, and screen state in Jetpack Compose, where a new copy of the state triggers the screen to redraw. Their value-based equality makes them easy to test, too: assertEquals(expectedUser, actualUser) just works.

Kotlin Data Classes FAQ

Can a Kotlin data class have methods?

Yes. A data class body can contain functions and computed properties like any other class, such as fun pounds() = pence / 100.0 on a Money class. Keep them about the data itself; behaviour that changes state over time usually belongs in a regular class.

Can a data class inherit from another class?

A data class can extend an open or abstract class and implement interfaces, which is common with sealed hierarchies. But it can't be open itself, so nothing can inherit from a data class. Writing open data class is a compile error.

Does copy() make a deep copy of a data class?

No, copy() is shallow. The new object gets the same property values, so if a property holds a MutableList, both objects share that one list and a change through either is visible in both. Using read-only List properties, and building new lists when things change, avoids the problem.

Can I override toString in a data class?

Yes. If you write your own override fun toString(), the compiler uses yours instead of generating one, and the same goes for equals and hashCode. A common reason is hiding sensitive fields, such as a password, so they never end up in logs.

Should I use a Pair or a data class?

A Pair is fine for a quick, local result, such as returning a minimum and maximum together. Once the values travel further through your code, a data class is clearer, because result.low says far more than result.first. Data classes also let you add more properties later without changing every caller.

Kotlin Data Classes Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the declaration so Point gets a generated toString and equals.

 class Point(val x: Int, val y: Int)
Predict the outputeasy

What does this print?

data class Pet(val name: String, val age: Int)
println(Pet("Rex", 3))
Predict the outputmedium

What does this print?

data class Size(val w: Int, val h: Int)
val a = Size(2, 3)
val b = a.copy(h = 9)
println("${a.h} ${b.h}")
Multiple choicemedium

Which properties does a data class use for equals and toString?

Multiple choicehard

Which is the best fit for a regular class rather than a data class?

Kotlin Data Classes Quiz

5 questions to check what you learned.

  1. What does copy() do on a data class?

    • Creates a new object, optionally with some properties changed
    • Copies it to the clipboard
    • Changes the original object
    • Deletes the object
  2. How does == compare two data class objects?

    • By memory address
    • By their names
    • It always returns false
    • By the values of their constructor properties
  3. How does destructuring decide which value goes where?

    • By property name
    • By property position in the constructor
    • Alphabetically
    • Randomly
  4. How many items does setOf(Point(1, 1), Point(1, 1)) contain, with Point a data class?

    • 2
    • 0
    • An error
    • 1
  5. What's the minimum requirement for a data class constructor?

    • At least one val or var parameter
    • No parameters
    • Exactly two parameters
    • All parameters must be var