Kotlin data classes and sealed interfaces

Model alternatives with a sealed hierarchy and let the compiler check that every case is handled.

Kotlin
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)
}
Output
Circle(radius=1.5) -> 7.07
Rectangle(width=2.0, height=3.5) -> 7.00
true
Circle(radius=3.0)
20.0

Explanation

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.

Explore the dry run
Guided walkthroughReady
1import java.util.Locale
2
3sealed interface Shape
4data class Circle(val radius: Double) : Shape
5data class Rectangle(val width: Double, val height: Double) : Shape
6
7fun area(shape: Shape): Double = when (shape) {
8 is Circle -> Math.PI * shape.radius * shape.radius
9 is Rectangle -> shape.width * shape.height
10}
11
12fun main() {
13 val shapes = listOf(Circle(1.5), Rectangle(2.0, 3.5))
14 for (shape in shapes) {
15 println(String.format(Locale.ROOT, "%s -> %.2f", shape, area(shape)))
16 }
17
18 val circle = Circle(2.0)
19 val bigger = circle.copy(radius = 3.0)
20 println(circle == Circle(2.0))
21 println(bigger)
22 val (width, height) = Rectangle(4.0, 5.0)
23 println(width * height)
24}

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
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput 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

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.

Further reading