Data Classes
Reviewed & published by Brayan K
Kotlin is a modern, concise language whose data classes hand you equals, hashCode, toString, copy, and destructuring for free — perfect for modelling plain data.
Part of the free Kotlin course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
By the end of this lesson you'll declare data classes and use content equality, copy(), and destructuring.
What You'll Learn in This Lesson
- What the data keyword generates
- Content equality with == vs identity with ===
- Readable toString() output
- Making modified copies with copy()
- Destructuring into variables
- The rules and limits of data classes
1️⃣ The data Keyword
Add data before class and the compiler generates toString(), equals(), hashCode(), copy(), and componentN() for you, all based on the constructor's properties.
// Add 'data' before class to get a bundle of features for free.
data class User(val name: String, val age: Int)
fun main() {
val a = User("Ada", 36)
// 1) toString() is generated — readable output instead of a hash.
println(a) // -> User(name=Ada, age=36)
// 2) equals() compares by CONTENT, not identity.
val b = User("Ada", 36)
println(a == b) // -> true (same data)
println(a === b) // -> false (different objects)
// 3) hashCode() is consistent with equals — safe in sets/maps.
val people = setOf(a, b)
println(people.size) // -> 1 (b is a duplicate of a)
}Because equals and hashCode are consistent, two equal users count as one entry in a Set — exactly what you'd expect.
2️⃣ copy() and Destructuring
copy() creates a new instance with selected properties changed, leaving the original alone — the immutable way to "update" data. Destructuring unpacks properties into separate variables in one line.
data class Point(val x: Int, val y: Int)
fun main() {
val origin = Point(0, 0)
// 4) copy() makes a modified duplicate without touching the original.
val moved = origin.copy(x = 5)
println(origin) // -> Point(x=0, y=0)
println(moved) // -> Point(x=5, y=0)
// 5) destructuring: pull properties straight into variables.
val (x, y) = moved
println("x=$x, y=$y") // -> x=5, y=0
// componentN() powers destructuring (component1() == x, etc.)
println(moved.component1()) // -> 5
// Great for returning multiple values from a function:
fun minMax(list: List<Int>) = Point(list.min(), list.max())
val (lo, hi) = minMax(listOf(3, 7, 1, 9))
println("lo=$lo hi=$hi") // -> lo=1 hi=9
}Returning a data class and destructuring it is a clean way to return multiple related values from a function, as the minMax example shows.
Your turn. Replace the TODO, then run and compare.
// 🎯 YOUR TURN — replace each TODO then run main()
// 1) Make this a data class so it gets toString, equals, copy, etc.
TODO class Book(val title: String, val pages: Int) // 👉 replace TODO with 'data'
fun main() {
val b1 = Book("Kotlin", 300)
val b2 = b1.copy(pages = 350) // copy with one field changed
println(b1) // expect Book(title=Kotlin, pages=300)
println(b2) // expect Book(title=Kotlin, pages=350)
println(b1 == Book("Kotlin", 300)) // expect true
val (title, pages) = b2 // destructuring
println("$title has $pages pages")
}Mini-Challenge: A Product Catalogue
Use copy() to apply a discount and destructuring to print a summary.
// 🎯 MINI-CHALLENGE: a product catalogue.
// Save as Product.kt and run main().
//
// 1. Define data class Product(val name: String, val price: Double).
// 2. Create a Product, then use copy() to make a 10%-off version
// (price = original.price * 0.9).
// 3. Print both products.
// 4. Destructure the discounted product into (name, price) and
// print "<name> now costs <price>".
//
// ✅ Example output:
// Product(name=Mug, price=10.0)
// Product(name=Mug, price=9.0)
// Mug now costs 9.0
fun main() {
// your code here
}Common Errors (and the fix)
- "Data class must have at least one primary constructor parameter" — Add a property to the constructor; an empty data class isn't allowed.
- Body properties ignored by equals — Only constructor properties count in generated functions. Put data you want compared in the constructor.
- Mutating instead of copying — With val properties you can't reassign; use copy() to make a changed version.
- Using === to compare data — That checks identity, not content. Use == for value equality.
Pro Tips
- 💡 Prefer val properties in data classes for immutable, predictable models.
- 💡 Update state with copy(): it's the standard pattern in Compose and reducers.
- 💡 Destructure map entries and pairs too — for ((k, v) in map) uses the same machinery.
📋 Quick Reference — Data Classes
| Concept | Kotlin Syntax |
|---|---|
| Declare | data class U(val n: String) |
| Content equality | a == b |
| Identity | a === b |
| Modified copy | u.copy(n = "Bob") |
| Destructure | val (n, a) = u |
🎉 Lesson Complete!
- ✅ data generates equals, hashCode, toString, copy, componentN
- ✅ == compares content; === compares identity
- ✅ copy() makes modified duplicates
- ✅ Destructuring unpacks properties in one line
- ✅ Only constructor properties feed the generated functions
Practice quiz
Which functions does the 'data' keyword generate for a class?
- Only toString()
- equals(), hashCode(), toString(), copy(), and componentN()
- Just a constructor
- Only copy()
Answer: equals(), hashCode(), toString(), copy(), and componentN(). data generates equals, hashCode, toString, copy, and componentN based on the constructor properties.
What does == compare for two data class instances?
- Their structural content via equals()
- Their memory addresses
- Their class names only
- Always returns false
Answer: Their structural content via equals(). == is structural equality and calls equals(), comparing by content.
What does === check in Kotlin?
- Content equality
- Type compatibility
- Referential identity (same object in memory)
- Hash code equality only
Answer: Referential identity (same object in memory). === is referential equality: whether two references point to the same object.
What does copy() do on a data class?
- Mutates the original in place
- Deletes the original
- Converts it to a map
- Creates a new instance with selected properties changed
Answer: Creates a new instance with selected properties changed. copy() returns a new instance with chosen fields overridden, leaving the original untouched.
Which functions power destructuring of a data class?
- componentN() functions like component1()
- getValue() and setValue()
- iterator()
- compareTo()
Answer: componentN() functions like component1(). Destructuring uses componentN(); component1() returns the first property, and so on.
Which properties feed the generated equals/hashCode/toString/copy?
- All properties including body ones
- Only properties declared in the primary constructor
- Only the first property
- Only val properties in the body
Answer: Only properties declared in the primary constructor. Only primary-constructor properties are used; body properties are ignored by those functions.
What is a requirement for a data class?
- It must be abstract
- It must be open
- It must have no parameters
- It must have a primary constructor with at least one parameter
Answer: It must have a primary constructor with at least one parameter. A data class needs a primary constructor with at least one val/var parameter.
Two separately created data class objects with identical data are...
- == true and === true
- == true and === false
- == false and === true
- == false and === false
Answer: == true and === false. They have equal content (== true) but are different objects (=== false).
Which is NOT allowed for a data class?
- Being declared as abstract, open, sealed, or inner
- Having val properties
- Using copy()
- Being destructured
Answer: Being declared as abstract, open, sealed, or inner. Data classes cannot be abstract, open, sealed, or inner.
How many entries do two equal data class instances occupy in a Set?
- Two entries
- Zero entries
- One entry
- It throws an error
Answer: One entry. Because equals and hashCode are consistent, equal instances count as a single Set entry.
Continue this course
- Previous: Properties
- Next: Inheritance & Interfaces — Build class hierarchies with open classes, abstract classes, and interfaces
- Quick reference: Kotlin cheat sheet › Classes & Objects
Frequently asked questions
What does the 'data' keyword actually generate?
Marking a class data tells the compiler to generate, based on the primary constructor's properties: a readable toString(), an equals() that compares by content, a matching hashCode(), a copy() function for making modified duplicates, and componentN() functions that enable destructuring. This replaces dozens of lines of boilerplate you would otherwise write by hand, and keeps the implementations correct and in sync.
How is == different from === in Kotlin?
== is structural equality: it calls equals(), so two data class instances with the same property values are equal. === is referential equality: it checks whether two references point to the exact same object in memory. For data classes, two separately created objects with identical data are == true but === false. In most application code you want ==.
Why is copy() useful, and how does it relate to immutability?
Data classes are often built from val properties, making them immutable. To 'change' one, you don't mutate it — you create a new instance with copy(), overriding just the fields you want: user.copy(age = 37). This keeps the original untouched, which makes code easier to reason about and is the standard pattern in state management. copy() lets you have immutability without tediously re-specifying every field.
Are there any rules or limits on data classes?
Yes. A data class must have a primary constructor with at least one parameter, and all primary constructor parameters must be marked val or var. Data classes cannot be abstract, open, sealed, or inner. Only the properties declared in the primary constructor are used for the generated equals, hashCode, toString, and copy — properties declared in the body are ignored by those functions. Within those rules they are extremely handy for modelling plain data.