Reading — step 1 of 7
Learn
Kotlin's data class and sealed class are language-level shortcuts that replace large chunks of boilerplate-heavy Java code.
data class — for value-type data
data class User(val name: String, val age: Int)
val ada = User("Ada", 36)
val ada2 = User("Ada", 36)
ada == ada2 // true — equals based on properties
ada.toString() // "User(name=Ada, age=36)"
ada.hashCode() // consistent with equals
val older = ada.copy(age = 37) // copy with one field changed
The compiler GENERATES:
equals()— structural equality based on all primary-constructor propertieshashCode()— matchingtoString()— readable representationcopy(...)— clone with optional field overridescomponentN()— for destructuring
Destructuring works automatically:
val (name, age) = ada
println("$name is $age")
When to use data classes:
- DTOs / model objects
- Tuples that need names (Pair / Triple are limited)
- Immutable value containers
Restrictions:
- Primary constructor must have at least one parameter (all val/var)
- Cannot be
abstract,open,sealed, orinner - Inheritance is restricted (no extending another data class)
sealed class — closed hierarchy
A sealed class restricts subclasses to those declared in the same file (or, in newer Kotlin, the same module):
sealed class Result<out T> {
data class Success<T>(val value: T) : Result<T>()
data class Failure(val error: String) : Result<Nothing>()
object Loading : Result<Nothing>()
}
fun <T> handle(r: Result<T>): String = when (r) {
is Result.Success -> "got ${r.value}"
is Result.Failure -> "error: ${r.error}"
Result.Loading -> "still loading"
}
// No `else` needed — compiler knows sealed has only those subclasses
Why sealed?
- Exhaustive
whenchecks — compiler errors if you miss a case - Compiler-known type hierarchy (better optimization, smarter type inference)
- Refactor-safe — add a new subclass and every
whenlights up
This is Kotlin's equivalent of Swift enums with associated values, or Rust enum variants.
sealed interface (Kotlin 1.5+)
Like sealed class but as an interface — can be implemented by classes or objects, AND a class can implement multiple sealed interfaces:
sealed interface JsonValue
data class JsonString(val value: String) : JsonValue
data class JsonNumber(val value: Double) : JsonValue
object JsonNull : JsonValue
object — singletons
Declare a one-and-only-one instance:
object Settings {
var theme: String = "dark"
fun reset() { theme = "dark" }
}
Settings.theme = "light"
Settings.reset()
No class definition, no getInstance() boilerplate. Thread-safe lazy initialization handled by the compiler.
Companion objects — class-level statics:
class User(val id: Int, val name: String) {
companion object {
fun fromJson(json: String): User {
// parse and return User
return User(0, "")
}
}
}
val u = User.fromJson("...") // call without an instance
Companion object methods serve as the equivalent of Java static methods.
Common mistakes
- Using
data classfor everything — when behavior is the focus, use a regular class. Data class is for value-equality data containers. - Forgetting
data classwhen you need equals/hashCode — regular classes use referential equality. Hidden bug source. - Adding fields outside the primary constructor in a data class — those fields aren't part of equals/hashCode/copy.
- Sealed without exhaustive when — if you use
elsein a when on sealed, the compiler can't help when you add a new subclass. - Confusing object expression with object declaration —
object : Listener { ... }is an anonymous inner-class-equivalent;object Foois a singleton.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…