Sealed classes and interfaces represent restricted hierarchies, which means hierarchies with a concrete set of classes that is known in advance. Great examples are BinaryTree (which is either Leaf or Node) and Either (which is either Left or Right). Such classes or interfaces have a concrete subset of children that will never change.
sealed interface BinaryTree
class Leaf(val value: Any?) : BinaryTree
class Node(val left: Tree, val right: Tree) : BinaryTree
sealed interface Either<out L, out R>
class Left<L>(val value: L) : Either<L, Nothing>
class Right<R>(val value: R) : Either<Nothing, R>
Sealed interfaces were introduced in Kotlin 1.5. Before that version, we had to use sealed classes whose subclasses had to be defined in the same file.
Sealed classes are also used to represent hierarchies that might change in the future but are considered final now, such as a set of operations we support, or the messages an actor accepts.
sealed class ValueChange<out T>
object Keep: ValueChange<Nothing>()
object SetDefault: ValueChange<Nothing>()
object SetEmpty: ValueChange<Nothing>()
class Set<out T>(val value: T): ValueChange<T>()
sealed class ManagerMessage
class CodeProduced(val code: String): ManagerMessage()
object ProductionStopped: ManagerMessage()
sealed interface AdView
object FacebookAd: AdView
object GoogleAd: AdView
class OwnAd(val text: String, val imgUrl: String): AdView
Notice that when a class does not hold any data, we use an object declaration. This is an optimization as it means we have one instance for all usages. Both creation and comparison are easier.
One might say that a sealed class or interface is a Kotlin way of expressing union types (sum types or coproducts) — a set of alternatives. For instance, Either is either Left or Right, but it is never both.
Sealed classes are abstract classes, but because of their characteristics they also have some additional restrictions on their subclasses:
they need to be defined in the same package and module where the sealed class is defined,
they can't be local or anonymous objects.
This means that when you use the sealed modifier, you control which subclasses a class or interface has. The clients of your library or module cannot add their own. No one can quietly add a local class or object expression that extends this class. The hierarchy of subclasses is restricted.
Sealed classes and when expressions
Using when as an expression forces us to return a value for every branch. In most cases, the only way to cover all possibilities is to specify an else clause. The power of having a finite type as an argument makes it possible to have an exhaustive when with a branch for every possible value, which with sealed classes means is checks for all possible subtypes. Also, IntelliJ automatically suggests adding the remaining branches. These two make sealed classes very convenient to check for all possible types.
sealed class Response<out R>
class Success<R>(val value: R): Response<R>()
class Failure(val error: Throwable): Response<Nothing>()
fun handle(response: Response<String>) {
val text = when (response) {
is Success -> "Success with ${response.value}"
is Failure -> "Error"
// else is not needed here
}
print(text)
}
Notice that when the else clause is not used, and when we add another subclass of this sealed class, the usage needs to be adjusted by including this new type. This is convenient in local code as it forces us to handle this new class in all exhaustive when expressions (so, typically in most cases where we need to). The inconvenient part is that when this sealed class is part of the public API of some library or shared module, adding a subtype is a breaking change.
Summary
Sealed classes and interfaces should be used to represent restricted hierarchies. They make it easier to handle each possible value and, as a result, to add new methods using extension functions. Abstract classes leave space for new classes to join this hierarchy. If we want to control what the subclasses of a class are, we should use the sealed modifier. We use abstract mainly when we design for inheritance.
Marcin Moskala is a highly experienced developer and Kotlin instructor as the founder of Kt. Academy, an official JetBrains partner specializing in Kotlin training, Google Developers Expert, known for his significant contributions to the Kotlin community. Moskala is the author of several widely recognized books, including "Effective Kotlin," "Kotlin Coroutines," "Functional Kotlin," "Advanced Kotlin," "Kotlin Essentials," and "Android Development with Kotlin."
Beyond his literary achievements, Moskala is the author of the largest Medium publication dedicated to Kotlin. As a respected speaker, he has been invited to share his insights at numerous programming conferences, including events such as Droidcon and the prestigious Kotlin Conf, the premier conference dedicated to the Kotlin programming language.
Nicola Corti is a Google Developer Expert for Kotlin. He has been working with the language since before version 1.0 and he is the maintainer of several open-source libraries and tools.
He's currently working as Android Infrastructure Engineer at Spotify in Stockholm, Sweden.
Furthermore, he is an active member of the developer community.
His involvement goes from speaking at international conferences about Mobile development to leading communities across Europe (GDG Pisa, KUG Hamburg, GDG Sthlm Android).
In his free time, he also loves baking, photography, and running.