Kotlin · Course navigation

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
Before you start

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

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

Follow the code

Open the walkthrough to inspect the example one step at a time.

Show guided walkthrough
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

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.

Further reading