Kotlin Sealed Classes
Sealed classes
Sealed classes and interfaces represent restricted class hierarchies that make inheritance easier to manage. “Sealed” means closed or sealed off, and a sealed class can be considered an extended form of an enum class that groups related classes. All direct subclasses of a sealed class are known at compile time. After the module containing the sealed class is compiled, no other subclasses appear. For example, third-party client code cannot extend a sealed class. Therefore, each instance of a sealed class has a type from a limited set that is known when the class is compiled.
The same applies to implementations of sealed interfaces. After a module with a sealed interface is compiled, new implementations cannot be added.
In a sense, sealed classes are similar to enum classes. The set of enum values is also limited, but each enum constant exists only as a single instance, whereas subclasses of a sealed class can each have multiple instances with their own state.
For example, consider the API of a library. It may include error classes so that library users can handle exceptions that may be thrown. If this exception class hierarchy includes an interface or abstract class exposed in the public API, client code cannot be prevented from implementing or extending it. However, because the library does not know about errors declared outside the library, it cannot handle those errors consistently in its own classes. With a sealed hierarchy of error classes, the library author can reliably know every possible error type and guarantee that no other error types will appear later.
To declare a sealed class or interface, add the sealed modifier before its name.
sealed interface Error
sealed class IOError(): Error
class FileReadError(val file: File): IOError()
class DatabaseError(val source: DataSource): IOError()
object RuntimeError : Error
A sealed class is abstract by itself, cannot be instantiated directly, and can have abstract members.
Constructors of sealed classes can have one of two visibility levels by default: protected or private.
sealed class IOError {
constructor() { /*...*/ } // protected by default
private constructor(description: String): this() { /*...*/ } // private is OK
// public constructor(code: Int): this() {} // Error: public and internal are not allowed
}
Location of direct subclasses
Direct subclasses of sealed classes and interfaces must be declared in the same package. They can be top-level declarations or nested inside another named class, named interface, or named object. Subclasses can have any visibility as long as they are compatible with Kotlin’s normal inheritance rules.
Subclasses of sealed classes must have proper qualified names. They cannot be local objects or anonymous objects.
Enum classes cannot extend sealed classes or other classes, but they can implement sealed interfaces.
These restrictions do not apply to indirect subclasses. If a direct subclass of a sealed class is not marked as sealed, it can be extended in any way allowed by its modifiers.
sealed interface Error // has implementations only in same package and module
sealed class IOError(): Error // extended only in same package and module
open class CustomError(): Error // can be extended wherever it's visible
Inheritance in multiplatform projects
Multiplatform projects have one more inheritance restriction. Direct subclasses of sealed classes must exist in the same source set. This applies to sealed classes that are not qualified with expect and actual.
If a sealed class is declared as expect in a common source set and has an actual implementation in a platform source set, both the expect and actual versions can have subclasses in their source sets. Also, if you use a hierarchical structure, subclasses can be created in any source set between the expect and actual declarations.
See more about hierarchies in multiplatform projects.
Sealed classes and when expressions
The main benefit of using sealed classes appears when using when. If the compiler can confirm that the statement covers all cases, you do not need to add an else clause. However, this works only when when is used as an expression whose result is used, not merely as a statement.
fun log(e: Error) = when(e) {
is FileReadError -> { println("Error while reading file ${e.file}") }
is DatabaseError -> { println("Error while reading from database ${e.source}") }
RuntimeError -> { println("Runtime error") }
// the `else` clause is not required because all the cases are covered
}
In common code for multiplatform projects, expressions over expected sealed classes may still require an else branch. This happens because subclasses from the actual platform implementation are not recognized by common code.