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:
interface Result
class Success(val data: String) : Result
class Failure(val exception: Throwable) : Result
Alternatively, you could use an abstract class:
abstract class Result
class Success(val data: String) : Result()
class Failure(val exception: Throwable) : Result()
With either of these, we know that when a function returns Result, it can be Success or Failure.
val result: Result = getSomeData()
when (result) {
is Success -> handleSuccess(result.data)
is Failure -> handleFailure(result.exception)
}
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 Result.
class FakeSuccess : Result
val res: Result = object : Result {}
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 interface[^14_0].
sealed interface Result
class Success(val data: String) : Result
class Failure(val exception: Throwable) : Result
// or
sealed class Result
class Success(val data: String) : Result()
class Failure(val exception: Throwable) : Result()
When we use the sealed modifier before a class, it makes this class abstract already, so we don’t use the abstract modifier.
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 subclasses[^14_2]. 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 expressions
Using 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 else clause.
fun commentValue(value: String) = when {
value.isEmpty() -> "Should not be empty"
value.length < 5 -> "Too short"
else -> "Correct"
}
fun main() {
println(commentValue("")) // Should not be empty
println(commentValue("ABC")) // Too short
println(commentValue("ABCDEF")) // Correct
}
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.
enum class PaymentType {
CASH,
CARD,
CHECK
}
fun commentDecision(type: PaymentType) = when (type) {
PaymentType.CASH -> "I will pay with cash"
PaymentType.CARD -> "I will pay with card"
PaymentType.CHECK -> "I will pay with check"
}
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.
sealed class Response<out V>
class Success<V>(val value: V) : Response<V>()
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)
}
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 value or error. Sealed subclasses can each store different data, whereas an enum is just a set of values.
Use cases
We use sealed classes whenever we want to express that there is a concrete number of subclasses of a class.
sealed class MathOperation
class Plus(val left: Int, val right: Int) : MathOperation()
class Minus(val left: Int, val right: Int) : MathOperation()
class Times(val left: Int, val right: Int) : MathOperation()
class Divide(val left: Int, val right: Int) : MathOperation()
sealed interface Tree
class Leaf(val value: Any?) : Tree
class Node(val left: Tree, val right: Tree) : Tree
sealed interface Either<out L, out R>
class Left<out L>(val value: L) : Either<L, Nothing>
class Right<out R>(val value: R) : Either<Nothing, R>
sealed interface AdView
object FacebookAd : AdView
object GoogleAd : AdView
class OwnAd(val text: String, val imgUrl: String) : AdView
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.
fun BinaryTree.height(): Int = when (this) {
is Leaf -> 1
is Node -> maxOf(this.left.height(), this.right.height())
}
However, expressing that a hierarchy is restricted improves readability. Finally, when we use the sealed modifier, we can use reflection to find all the subclasses[^14_1]:
sealed interface Parent
class A : Parent
class B : Parent
class C : Parent
fun main() {
println(Parent::class.sealedSubclasses)
// [class A, class B, class C]
}
Summary
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 sealed modifier.
Next, we will talk about the last special kind of class that is used to add extra information about our code elements: annotations.
[^14_0]: 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.
[^14_1]: This requires the kotlin-reflect dependency. More about reflection in Advanced Kotlin.
[^14_2]: 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.
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.
Owen has been developing software since the mid 1990s and remembers the productivity of languages such as Clipper and Borland Delphi.
Since 2001, He moved to Web, Server based Java and the Open Source revolution.
With many years of commercial Java experience, He picked up on Kotlin in early 2015.
After taking detours into Clojure and Scala, like Goldilocks, He thinks Kotlin is just right and tastes the best.
Owen enthusiastically helps Kotlin developers continue to succeed.
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.
Software architect with 15 years of experience, currently working on building infrastructure for AI. I think Kotlin is one of the best programming languages ever created.
Emanuele is passionate about Android and has been fascinated by it since 2010: the more he learns, the more he wishes to share what he knows with others, which is why he started maintaining his own blog.
In his current role as Senior Android Developer at Mozio, he is now focusing on Kotlin Multiplatform Mobile: he has already given a couple of talks on this topic on various occasions, so far.
Interested in everything Android and Kotlin related, including architecture patterns, TDD, functional programming and Jetpack Compose. Author of several articles about Android and Kotlin Coroutines.