Data Classes
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.
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)
}Ticket(passenger=Ananya, from=Lucknow, to=Delhi, fare=785) true false true false
toStringprints the class name and every property, which is perfect for logging and debugging.==compares the values.t1andt2are two separate objects (===isfalse), 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
PlainTickethas 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.
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)
}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.
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")
}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.
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)])
}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.
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)
}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.
@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
}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
varproperties everywhere. Mutable data classes break sets and maps and make bugs harder to find. Usevalandcopy. - Getting destructuring order wrong.
val (marks, name) = resultsilently puts the name intomarksif the types allow it. Destructuring follows constructor order, not variable names. - Expecting body properties in
toStringorequals. 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 classgeneratestoString,equals,hashCode,copyandcomponentNfrom 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 objectgives a single object a readabletoString.@JvmInline value classwraps 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
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")
}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?
Data classes get equals, hashCode, toString, copy and componentN.
2. Two data class objects have identical property values. What does a == b return?
== uses the generated equals, which compares the constructor properties.
3. What does p.copy(city = "Agra") do?
copy creates a new object; the original is unchanged.
4. How does destructuring decide which value goes into which variable?
Destructuring calls component1(), component2() and so on, in constructor order.
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
