Sealed classes and interfaces
Sealed classes and interfaces provide controlled inheritance of your class hierarchies. All direct subclasses of a sealed class are known at compile time. No other subclasses may appear outside the module and package within which the sealed class is defined. The same logic applies to sealed interfaces and their implementations: once a module with a sealed interface is compiled, no new implementations can be created.
When you combine sealed classes and interfaces with the when
expression, you can cover the behavior of all possible subclasses and ensure that no new subclasses are created to affect your code adversely.
Sealed classes are best used for scenarios when:
Limited class inheritance is desired: You have a predefined, finite set of subclasses that extend a class, all of which are known at compile time.
Type-safe design is required: Safety and pattern matching are crucial in your project. Particularly for state management or handling complex conditional logic. For an example, check out Use sealed classes with when expressions.
Working with closed APIs: You want robust and maintainable public APIs for libraries that ensure that third-party clients use the APIs as intended.
For more detailed practical applications, see Use case scenarios.
Declare a sealed class or interface
To declare a sealed class or interface, use the sealed
modifier:
This example could represent a library's API that contains error classes to let library users handle errors that it can throw. If the hierarchy of such error classes includes interfaces or abstract classes visible in the public API, then nothing prevents other developers from implementing or extending them in the client code. Since the library doesn't know about errors declared outside of it, it can’t treat them consistently with its own classes. However, with a sealed hierarchy of error classes, library authors can be sure that they know all the possible error types and that other error types can't appear later.
The hierarchy of the example looks like this:
Constructors
A sealed class itself is always an abstract class, and as a result, can't be instantiated directly. However, it may contain or inherit constructors. These constructors aren't for creating instances of the sealed class itself but for its subclasses. Consider the following example with a sealed class called Error
and its several subclasses, which we instantiate:
You can use enum
classes within your sealed classes to use enum constants to represent states and provide additional detail. Each enum constant exists only as a single instance, while subclasses of a sealed class may have multiple instances. In the example, the sealed class Error
along with its several subclasses, employs an enum
to denote error severity. Each subclass constructor initializes the severity
and can alter its state:
Constructors of sealed classes can have one of two visibilities: protected
(by default) or private
:
Inheritance
Direct subclasses of sealed classes and interfaces must be declared in the same package. They may be top-level or nested inside any number of other named classes, named interfaces, or named objects. Subclasses can have any visibility as long as they are compatible with normal inheritance rules in Kotlin.
Subclasses of sealed classes must have a properly qualified name. They can't be local or anonymous objects.
These restrictions don't apply to indirect subclasses. If a direct subclass of a sealed class is not marked as sealed, it can be extended in any way that its modifiers allow:
Inheritance in multiplatform projects
There is one more inheritance restriction in multiplatform projects: direct subclasses of sealed classes must reside in the same source set. It applies to sealed classes without the expected and actual modifiers.
If a sealed class is declared as expect
in a common source set and have actual
implementations in platform source sets, both expect
and actual
versions can have subclasses in their source sets. Moreover, if you use a hierarchical structure, you can create subclasses in any source set between the expect
and actual
declarations.
Learn more about the hierarchical structure of multiplatform projects.
Use sealed classes with when expression
The key benefit of using sealed classes comes into play when you use them in a when
expression. The when
expression, used with a sealed class, allows the Kotlin compiler to check exhaustively that all possible cases are covered. In such cases, you don't need to add an else
clause:
When using sealed classes with when
expressions, you can also add guard conditions to include additional checks in a single branch. For more information, see Guard conditions in when expressions.
Use case scenarios
Let's explore some practical scenarios where sealed classes and interfaces can be particularly useful.
State management in UI applications
You can use sealed classes to represent different UI states in an application. This approach allows for structured and safe handling of UI changes. This example demonstrates how to manage various UI states:
Payment method handling
In practical business applications, handling various payment methods efficiently is a common requirement. You can use sealed classes with when
expressions to implement such business logic. By representing different payment methods as subclasses of a sealed class, it establishes a clear and manageable structure for processing transactions:
Payment
is a sealed class that represents different payment methods in an e-commerce system: CreditCard
, PayPal
, and Cash
. Each subclass can have its specific properties, like number
and expiryDate
for CreditCard
, and email
for PayPal
.
The processPayment()
function demonstrates how to handle different payment methods. This approach ensures that all possible payment types are considered, and the system remains flexible for new payment methods to be added in the future.
API request-response handling
You can use sealed classes and sealed interfaces to implement a user authentication system that handles API requests and responses. The user authentication system has login and logout functionalities. The ApiRequest
sealed interface defines specific request types: LoginRequest
for login, and LogoutRequest
for logout operations. The sealed class, ApiResponse
, encapsulates different response scenarios: UserSuccess
with user data, UserNotFound
for absent users, and Error
for any failures. The handleRequest
function processes these requests in a type-safe manner using a when
expression, while getUserById
simulates user retrieval: