Kotlin Sealed Classes In Action

s1m0nw1
1,619 views

Open Source Your Knowledge, Become a Contributor

Technology knowledge has to be shared and made accessible for free. Join the movement.

Create Content

Sealed Classes in Kotlin

Disclaimer: My articles are published under "Attribution-NonCommercial-NoDerivatives 4.0 International (CC BY-NC-ND 4.0)".

© Copyright: Simon Wirtz, 2017

Feel free to share.

Intro

'Sealed Classes' is a feature of the Kotlin programming language, which is also available in Scala for example. Although many people haven't heard of it yet, it's a quite simple feature worth knowing, which I'm going to explain in the following.

Feature Explanation

A sealed class can be subclassed and may include abstract methods which means sealed classes are abstract implicitly, although the documentation doesn't clearly say so. To actually make a class "sealed" we have to put the sealed modifier before its name, as we can see here:

sealed class MyClass
Restriction

The important thing about sealed classes is that its subclasses must be declared in the same file as the sealed class itself.

Benefit

The feature allows us to define class hierarchies that are restricted in its' types, i.e. subclasses. Since all subclasses need to be defined inside the file of the sealed class, there's no chance of unknown subclasses which the compiler doesn't know about.

Wait... Isn't this what an enum actually is?

Kotlin's sealed classes are some kind of "extension" of plain enums: As opposed to enums, subclasses of sealed classes can be instantiated multiple types and can actually contain state.

Use Case

The main advantage of sealed classes reveals itself if it's used in when expressions. Let's compare a normal class hierarchy to one of a sealed class handled in a when. First, we'll create a hierarchy of Mammals and then put it into a method using a when statement:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fun main(args: Array<String>) {
greetMammal(Cat("Lucy")).toConsole()
greetMammal(Human("Luke", "Plumper")).toConsole()
greetMammal(object : Mammal("unknownMammal") {}).toConsole()
}
open class Mammal(val name: String)
class Cat(catName: String) : Mammal(catName)
class Human(humanName: String, val job: String) : Mammal(humanName)
fun greetMammal(mammal: Mammal): String = when (mammal) {
is Human -> "Hello ${mammal.name}; You're working as a ${mammal.job}"
is Cat -> "Hello ${mammal.name}"
else -> "Hello unknown"
}
//For debugging only
fun String.toConsole() = println(this)
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

The else is mandatory, otherwise the compiler would complain with "when expression must be exhaustive, add necessary else branch". This is because it cannot verify that all possible cases, i.e. subclasses, are covered here. It may be possible that a subclass Dog is available at any time that is still unknown at compile time.

Sealed "to the rescue"

What if we knew there wouldn't be other Mammals in our application? We'd want to leave out that ``else` branch.

The problem of unknown subclasses can be avoided by sealed classes. Let's modify the base class Mammal, its' subclasses can remain the same.

sealed class Mammal(val name: String)

The code example from above now looks as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fun main(args: Array<String>) {
greetMammal(Cat("Lucy")).toConsole()
greetMammal(Human("Luke", "Plumper")).toConsole()
}
sealed class Mammal(val name: String)
class Cat(catName: String) : Mammal(catName)
class Human(humanName: String, val job: String) : Mammal(humanName)
fun greetMammal(mammal: Mammal): String {
when (mammal) {
is Human -> return "Hello ${mammal.name}; You're working as a ${mammal.job}"
is Cat -> return "Hello ${mammal.name}"
// `else` clause not required
}
}
//For debugging only
fun String.toConsole() = println(this)
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

We can simply omit the else branch since the compiler can verify that all possible cases are covered. Only the subclasses defined in the file of the sealed class exist, without exception.

What if a branch for at least on subsclass is being omitted?

Let's see what happens if we don't add a when case for every subclass of our sealed class, e.g. do not have a Cat case:

1
2
3
4
5
6
7
8
9
10
11
sealed class Mammal(val name: String)
class Cat(catName: String) : Mammal(catName)
class Human(humanName: String, val job: String) : Mammal(humanName)
fun greetMammal(mammal: Mammal): String {
when (mammal) {
is Human -> return "Hello ${mammal.name}; You're working as a ${mammal.job}"
}
return "will not compile"
}
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

As one might expect, the compiler complains with a comprehensive error message: "Error: 'when' expression must be exhaustive, add necessary 'is Cat' branch or 'else' branch instead".

That's it. In conclusion really simple and handy, isn't it? Have fun trying it yourself!

Finally, if you want to read about sealed classes or any other feature in more detail I recommend the book Kotlin in Action.

Cheers, Simon

Open Source Your Knowledge: become a Contributor and help others learn. Create New Content