Sealed classes and interfaces in Kotlin
Classes and interfaces in Kotlin are not only used to represent a set of operations or data; we can also use classes and inheritance to represent hierarchies through polymorphism. For instance, let's say that you send a network request; as a result, you either successfully receive the requested data, or the request fails with some information about what went wrong. These two outcomes can be represented using two classes that implement an interface:
Alternatively, you could use an abstract class:
With either of these, we know that when a function returns
Result, it can be
The problem is that when a regular interface or abstract class is used, there is no guarantee that its defined subclasses are all possible subtypes of this interface or abstract class. Someone might define another class and make it implement
Result. Someone might also implement an object expression that implements
A hierarchy whose subclasses are not known in advance is known as a non-restricted hierarchy. For
Result, we prefer to define a restricted hierarchy, which we do by using a
sealed modifier before a class or an interface0.
When we use the
sealedmodifier before a class, it makes this class abstract already, so we don’t use the
There are a few requirements that all sealed class or interface children must meet:
- they need to be defined in the same package and module where the sealed class or interface is,
- they can't be local or defined using object expression.
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 direct subclasses2. No one can quietly add a local class or object expression that extends a sealed class or interface. Kotlin has made this impossible. The hierarchy of subclasses is restricted.
Sealed interfaces were introduced in more recent versions of Kotlin to allow classes to implement multiple sealed hierarchies. The relation between a sealed class and a sealed interface is similar to the relation between an abstract class and an interface. The power of classes is that they can keep a state (non-abstract properties) and control their members' openness (can have final methods and properties). The power of interfaces is that a class can inherit from only one class but it can implement multiple interfaces.
Sealed classes and
when as an expression must return some value, so it must be exhaustive. In most cases, the only way to achieve this is to specify an
However, there are also cases in which Kotlin knows that we have specified all possible values. For example, when we use a when-expression with an enum value and we compare this value to all possible enum values.
The power of having a finite set of types as an argument makes it possible to have an exhaustive
when with a branch for every possible value. In the case of sealed classes or interfaces, this means having
is checks for all possible subtypes.
Also, IntelliJ automatically suggests adding the remaining branches. This makes sealed classes very convenient when we need to cover all possible types.
Note that when an
else clause is not used and 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 exhaustive
when expressions. The inconvenient part is that when this sealed class is part of the public API of a library or shared module, adding a subtype is a breaking change because all modules that use exhaustive
when need to cover one more possible type.
Sealed vs enum
Enum classes are used to represent a set of values. Sealed classes or interfaces represent a set of subtypes that can be made with classes or object declarations. This is a significant difference. A class is more than a value. It can have many instances and can be a data holder. Think of
Response: if it were an enum class, it couldn't hold
error. Sealed subclasses can each store different data, whereas an enum is just a set of values.
We use sealed classes whenever we want to express that there is a concrete number of subclasses of a class.
The key benefit is that when-expression can easily cover all possible types in a hierarchy using is-checks. A when-condition with a sealed element as a value ensures the compiler performs exhaustive type checking, and our program can only represent valid states.
However, expressing that a hierarchy is restricted improves readability. Finally, when we use the
sealed modifier, we can use reflection to find all the subclasses1:
Sealed classes and interfaces should be used to represent restricted hierarchies. The when-statement makes it easier to handle each possible sealed subtype and, as a result, to add new methods to sealed elements 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
Next, we will talk about the last special kind of class that is used to add extra information about our code elements: annotations.
Restricted hierarchies are used to represent values that could take on several different but fixed types. In other languages, restricted hierarchies might be represented by sum types, coproducts, or tagged unions.
This requires the
kotlin-reflect dependency. More about reflection in Advanced Kotlin.
You could still declare an abstract class or an interface as a part of a sealed hierarchy that the client would be able to inherit from another module.