Data Classes

Kotlin · Lesson 16 of 29 7 min read Updated

Many classes exist only to hold data: a ticket, an order line, an exam result, a city's weather reading. For such classes you usually want the same things: a readable toString, equality based on the values, and an easy way to make a modified copy. Writing these by hand is boring and easy to get wrong.

Kotlin's data classes generate all of it for you from one line. In this lesson you will learn what a data class gives you, how copy and destructuring work, and two related tools: data object and value classes.

What data gives you

Put data in front of class, and the compiler generates toString(), equals(), hashCode(), copy() and componentN() functions based on the properties in the primary constructor.

Kotlin
data class Ticket(val passenger: String, val from: String, val to: String, val fare: Int)

class PlainTicket(val passenger: String, val fare: Int)

fun main() {
    val t1 = Ticket("Ananya", "Lucknow", "Delhi", 785)
    val t2 = Ticket("Ananya", "Lucknow", "Delhi", 785)
    println(t1)
    println(t1 == t2)
    println(t1 === t2)
    println(t1.hashCode() == t2.hashCode())

    val p1 = PlainTicket("Ananya", 785)
    val p2 = PlainTicket("Ananya", 785)
    println(p1 == p2)
}
Output
Ticket(passenger=Ananya, from=Lucknow, to=Delhi, fare=785)
true
false
true
false
  • toString prints the class name and every property, which is perfect for logging and debugging.
  • == compares the values. t1 and t2 are two separate objects (=== is false), but they hold the same data, so they are equal.
  • Equal objects have equal hash codes, so data classes work correctly as set items and map keys.
  • The ordinary class PlainTicket has none of this: == only checks whether both variables point to the same object.

The rules: the primary constructor needs at least one parameter, every parameter must be val or var, and a data class cannot be open, abstract or sealed. It can still implement interfaces and have functions in its body.

copy: changing data without changing objects

Data classes work best with val properties, so an object never changes after creation. To "change" one, you make a modified copy. copy() takes named arguments for the properties you want to change and keeps the rest.

Kotlin
data class Profile(val name: String, val city: String, val verified: Boolean = false)

fun main() {
    val before = Profile("Farhan", "Kochi")
    val moved = before.copy(city = "Pune")
    val checked = moved.copy(verified = true)

    println(before)
    println(moved)
    println(checked)
}
Output
Profile(name=Farhan, city=Kochi, verified=false)
Profile(name=Farhan, city=Pune, verified=false)
Profile(name=Farhan, city=Pune, verified=true)

The original before is untouched. Unchanging (immutable) objects are easier to reason about: no other part of the program can modify them behind your back, and they are safe to share between threads. This style is also how state is managed in Jetpack Compose apps.

Destructuring declarations

A data class can be unpacked into separate variables in one line. This is a destructuring declaration. It works by position: the first variable gets the first constructor property, and so on. Use _ to skip a property.

Kotlin
data class Result(val student: String, val marks: Int, val grade: Char)

fun topResult() = Result("Isha", 96, 'A')

fun main() {
    val (name, marks, grade) = topResult()
    println("$name got $marks ($grade)")

    val (who, _, g) = Result("Rohan", 58, 'C')
    println("$who: $g")

    val results = listOf(Result("Aarav", 81, 'A'), Result("Meera", 67, 'B'))
    for ((student, m) in results) {
        println("$student -> $m")
    }
    results.forEach { (student, _, grd) -> println("$student has grade $grd") }

    val (city, pin) = "Rohtak" to 124001
    println("$city $pin")
}
Output
Isha got 96 (A)
Rohan: C
Aarav -> 81
Meera -> 67
Aarav has grade A
Meera has grade B
Rohtak 124001

Destructuring is a neat way for a function to return several values: return a small data class and unpack it at the call site. Note the brackets in the lambda: { (student, _, grd) -> ... } destructures the single parameter, while { a, b -> ... } would mean two parameters. Pair and Triple can be destructured too, as the last lines show.

Data classes in sets and maps

Because equals and hashCode depend on the values, two separately created data objects with the same values count as the same item in a set, and as the same key in a map.

Kotlin
data class Seat(val coach: String, val number: Int)

fun main() {
    val booked = mutableSetOf(Seat("S1", 23), Seat("S1", 24))
    println(Seat("S1", 23) in booked)
    booked.add(Seat("S1", 23))
    println(booked.size)

    val passengers = mapOf(Seat("S1", 23) to "Kabir", Seat("S2", 5) to "Priya")
    println(passengers[Seat("S2", 5)])
}
Output
true
2
Priya

This only works reliably if the properties never change. If you put an object in a set and then change a var property, its hash code changes and the set may no longer find it. Another reason to prefer val.

Properties in the body and data objects

Only properties declared in the primary constructor take part in the generated functions. A property declared in the class body is ignored by toString, equals, hashCode and copy.

For a single, fixed value with a nice printed name, use a data object: an object declaration (a class with exactly one instance, covered in Objects & Companion Objects) that also gets a readable toString.

Kotlin
data class Cart(val owner: String) {
    var itemCount = 0
}

data object NoDiscount

fun main() {
    val a = Cart("Isha")
    a.itemCount = 3
    val b = Cart("Isha")
    println(a == b)
    println(a)
    println(NoDiscount)
}
Output
true
Cart(owner=Isha)
NoDiscount

a and b are equal even though their itemCount differs, because itemCount is not in the constructor. A plain object NoDiscount would print something like NoDiscount@4e50df2e; the data version prints just its name. Data objects appear often in sealed hierarchies, as you will see in the next lesson.

Advanced: value classes

Sometimes you want a type that wraps a single value for safety, such as Paise instead of a bare Long, so you cannot accidentally pass a phone number where an amount is expected. A value class gives you that type check at compile time, and on the JVM the wrapper usually disappears at run time, so it costs nothing.

Kotlin
@JvmInline
value class Paise(val value: Long)

fun charge(amount: Paise) {
    println("Charging ${amount.value} paise")
}

fun main() {
    charge(Paise(129_900))
    // charge(129_900)   // does not compile: a number is not Paise
}
Output
Charging 129900 paise

A value class must have exactly one property in its primary constructor. The @JvmInline annotation is required when targeting the JVM.

Common mistakes

  • Using var properties everywhere. Mutable data classes break sets and maps and make bugs harder to find. Use val and copy.
  • Getting destructuring order wrong. val (marks, name) = result silently puts the name into marks if the types allow it. Destructuring follows constructor order, not variable names.
  • Expecting body properties in toString or equals. Put everything that defines the value in the primary constructor.
  • Trying to inherit from a data class. Data classes are final. Share behaviour through interfaces or sealed parents instead.

Key points

  • data class generates toString, equals, hashCode, copy and componentN from the primary constructor.
  • == on data classes compares values; === still checks object identity.
  • copy(prop = newValue) creates a modified copy, keeping objects immutable.
  • Destructuring (val (a, b) = obj) is by position; _ skips a value.
  • data object gives a single object a readable toString.
  • @JvmInline value class wraps one value for type safety at almost no cost.

Exercise

Create a data class MenuItem(val name: String, val price: Int, val veg: Boolean). Make a list of four items for a Jaipur restaurant. Print the list sorted by price, then create a "festival offer" list where every item's price is reduced by 10 percent using copy. Finally, destructure the cheapest offer item into name and price and print them.

Show solution
Kotlin
data class MenuItem(val name: String, val price: Int, val veg: Boolean)

fun main() {
    val menu = listOf(
        MenuItem("Dal Baati", 220, true),
        MenuItem("Laal Maas", 380, false),
        MenuItem("Ghevar", 120, true),
        MenuItem("Ker Sangri", 180, true),
    )
    menu.sortedBy { it.price }.forEach { println(it) }

    val offer = menu.map { it.copy(price = it.price * 90 / 100) }
    val (name, price) = offer.minBy { it.price }
    println("Cheapest on offer: $name at Rs $price")
}
Output
MenuItem(name=Ghevar, price=120, veg=true)
MenuItem(name=Ker Sangri, price=180, veg=true)
MenuItem(name=Dal Baati, price=220, veg=true)
MenuItem(name=Laal Maas, price=380, veg=false)
Cheapest on offer: Ghevar at Rs 108

copy leaves the original menu unchanged. Destructuring takes the first two properties, name and price, and ignores veg.

Quick Quiz

1. Which function is NOT generated for a data class?

2. Two data class objects have identical property values. What does a == b return?

3. What does p.copy(city = "Agra") do?

4. How does destructuring decide which value goes into which variable?

Learn Kotlin with a mentor

Free lessons get you started. Our Programming & Software Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call