Sealed classes are used to build restricted class hierarchies, in the sense that all direct subtypes of the sealed class must be defined inside the same file as the sealed class itself.
Sealed classes are useful when you want to make sure that the values of a given type can only come from a particular limited set of subtypes. They allow you to define a strict hierarchy of types. The sealed classes themselves are abstract and cannot be instantiated.
That is, we have a class with a specific number of subclasses. What we get in the end is a concept very similar to an enum. The difference is that in the enum we only have one object per type, while in the sealed classes we can have several objects of the same class. As opposed to enums, subclasses of sealed classes can be instantiated multiple times and can actually contain state.
The important thing about sealed classes is that its subclasses must be declared in the same file as the sealed class itself.
The sealed class feature allows us to define class hierarchies that are restricted in their 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.
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 in a method with a when
:
open class Mammal(val name: String) class Cat(val catName: String) : Mammal(catName) class Human(val 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 -> return "Hello unknown" } }
The else
is mandatory, otherwise, the compiler will complain. This is because it just 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 which is unknown at compile time.
But what if we knew there wouldn’t be other Mammals
in our application? We’d want to leave out the else
block.
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)
Now we can simply omit the else
clause since the compiler can verify that all possible cases are covered because only the subclasses in the file of the sealed
class exist, without exception. The method now looks as follows:
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}" } }
If you leave any of the subclasses out, when will complain and it won’t compile. If you implement them all, you don’t need else statement. And in general it won’t be recommended because that way we’re sure that we’re doing the right thing for all of them.
The sealed class itself is implicitly abstract (thus cannot be instantiated) and its constructor is private (thus the class cannot be inherited from in another file). This already implies that all direct subtypes must be declared inside the same file. However, subtypes of subtypes may be declared anywhere. You could add abstract or concrete members to the sealed class, just like in other abstract classes.
The benefit of sealed classes is that you have more control over your class hierarchy, and the compiler uses this as well. Because all direct child classes are known at compile time, the compiler can check when
expressions for exhaustiveness. Hence, you can omit the else
branch whenever you cover all cases.
Sealed class rules
Sealed classes vs. enum classes
A sealed class has a limited number of direct subclasses, all defined in the same file as the sealed class itself. It’s known as sealed as opposed to final, since although some subclassing is permitted, the subclassing is extremely limited in scope.
The hope is that this technique allows programmers to take advantage of some of the flexibility of subclassing without permitting them to create massive inheritance trees which lead to terrible, incomprehensible code. There are a few key points to know about sealed classes:
Imagine you’re working for a company that mostly works in U.S. dollars, but also accepts payments in Euros and some form of cryptocurrency.
sealed class AcceptedCurrency { abstract val valueInDollars: Float var amount: Float = 0.0f class Dollar: AcceptedCurrency() { override val valueInDollars = 1.0f } class Euro: AcceptedCurrency() { override val valueInDollars = 1.25f } class Crypto: AcceptedCurrency() { override val valueInDollars = 2534.92f } val name: String get() = when (this) { is Euro -> "Euro" is Dollar -> "Dollars" is Crypto -> "NerdCoin" } fun totalValueInDollars(): Float { return amount * valueInDollars } }
In the main() function, add the following lines to the bottom:
val currency = AcceptedCurrency.Crypto() currency.amount = .27541f println("${currency.amount} of ${currency.name} is " + "${currency.totalValueInDollars()} in Dollars")