Kotlin data classes and sealed interfaces
Model alternatives with a sealed hierarchy and let the compiler check that every case is handled.
On this page
Kotlin classes and when expressions.
- Model alternatives with a sealed hierarchy and let the compiler check that every case is handled.
- Predict the sample output and explain the state changes.
- Complete the practice task and check the boundary cases.
The concept
A data class generates equals, hashCode, toString, copy, and componentN functions from its constructor properties. Two data class instances with the same values are equal, and copy makes a new instance with some properties changed. They are the usual way to model plain data in Kotlin.
A sealed interface (or class) restricts its direct subtypes to those declared in the same package and module. A when over a sealed type, used as an expression, must cover every subtype, and the compiler reports an error if a case is missing. So adding a new shape forces every when to be updated. No else branch is needed, which means no silent default for forgotten cases.
Code example
import java.util.Locale
sealed interface Shape
data class Circle(val radius: Double) : Shape
data class Rectangle(val width: Double, val height: Double) : Shape
fun area(shape: Shape): Double = when (shape) {
is Circle -> Math.PI * shape.radius * shape.radius
is Rectangle -> shape.width * shape.height
}
fun main() {
val shapes = listOf(Circle(1.5), Rectangle(2.0, 3.5))
for (shape in shapes) {
println(String.format(Locale.ROOT, "%s -> %.2f", shape, area(shape)))
}
val circle = Circle(2.0)
val bigger = circle.copy(radius = 3.0)
println(circle == Circle(2.0))
println(bigger)
val (width, height) = Rectangle(4.0, 5.0)
println(width * height)
}Circle(radius=1.5) -> 7.07 Rectangle(width=2.0, height=3.5) -> 7.00 true Circle(radius=3.0) 20.0
Follow the code
Open the walkthrough to inspect the example one step at a time.
Show guided walkthrough
import java.util.Locale sealed interface Shapedata class Circle(val radius: Double) : Shapedata class Rectangle(val width: Double, val height: Double) : Shape fun area(shape: Shape): Double = when (shape) { is Circle -> Math.PI * shape.radius * shape.radius is Rectangle -> shape.width * shape.height} fun main() { val shapes = listOf(Circle(1.5), Rectangle(2.0, 3.5)) for (shape in shapes) { println(String.format(Locale.ROOT, "%s -> %.2f", shape, area(shape))) } val circle = Circle(2.0) val bigger = circle.copy(radius = 3.0) println(circle == Circle(2.0)) println(bigger) val (width, height) = Rectangle(4.0, 5.0) println(width * height)}Follow the example
Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.
View the complete dry-run table
| Step / line | What happens | State after step | Output so far |
|---|---|---|---|
| Only the subtypes declared here can implement Shape. | — | — | |
| A data class: equality, copy, toString, and destructuring come for free. | — | — | |
| when as an expression over a sealed type must be exhaustive. There is no else branch. | — | — | |
| The generated toString shows the values. | — | Circle(radius=1.5) -> 7.07 Rectangle(width=2.0, height=3.5) -> 7.00 | |
| == calls equals, which compares the properties. | — | Circle(radius=1.5) -> 7.07 Rectangle(width=2.0, height=3.5) -> 7.00 true | |
| copy changes one property and keeps the rest. | — | Circle(radius=1.5) -> 7.07 Rectangle(width=2.0, height=3.5) -> 7.00 true | |
| The copy has radius 3.0. The original circle is unchanged. | — | Circle(radius=1.5) -> 7.07 Rectangle(width=2.0, height=3.5) -> 7.00 true Circle(radius=3.0) | |
| Destructuring uses the generated component functions. | — | Circle(radius=1.5) -> 7.07 Rectangle(width=2.0, height=3.5) -> 7.00 true Circle(radius=3.0) | |
| 4.0 times 5.0. | — | Circle(radius=1.5) -> 7.07 Rectangle(width=2.0, height=3.5) -> 7.00 true Circle(radius=3.0) 20.0 |
Behind the scenes
How Kotlin processes this code
Kotlin commonly compiles to JVM bytecode, which the JVM executes.
Cost and common mistakes
O(1) per call.
- A data class should have immutable properties (val). Mutable properties make equality and hash codes unstable inside sets and maps.
- copy is shallow. A mutable list inside the data class is shared between the original and the copy.
- Adding a new Shape subtype makes the when in area fail to compile until you handle it, which is the point.
Try it yourself
What happens if you add object Triangle : Shape and do not update area?
Reveal answer and reasoning
The code stops compiling, because the when expression is no longer exhaustive.