Sealed Classes and Enums

This section documents Kotlin 2.4.x on the JVM, as published at kotlinlang.org, which is the reference these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against kotlinlang.org before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

sealed and enum both describe a closed set of possibilities, and both let a when expression over them be checked for exhaustiveness at compile time — catching a forgotten case the moment a new variant is added rather than at run time.

sealed class and sealed interface

A sealed type restricts its subtypes to those declared in the same module and package — the compiler knows the complete, closed set of possibilities, unlike an ordinary open class or interface, whose subtypes could be anywhere:

sealed interface PaymentResult

data class Approved(val confirmationCode: String) : PaymentResult
data class Declined(val reason: String) : PaymentResult
object Pending : PaymentResult                          // see Objects and Companion Objects

sealed subtypes are commonly data class`es carrying variant-specific data, plus `object for a variant with no data at all — exactly the modeling job Java’s sealed + record combination was later added to cover, and the shape most naturally suited to representing "one of several distinct outcomes" (an API response, a UI state, a parse result).

Exhaustive when

A when expression over a sealed subject needs no else branch once every subtype is covered — the compiler verifies exhaustiveness and raises a compile error (not just a warning) if a new subtype is added later and a when site is not updated to handle it:

fun describe(result: PaymentResult): String = when (result) {
    is Approved -> "approved: ${result.confirmationCode}"   // smart-cast to Approved inside this branch
    is Declined -> "declined: ${result.reason}"
    Pending -> "still processing"
    // no "else" needed -- and adding a new PaymentResult subtype later
    // makes this "when" a compile error until it is updated
}

This exhaustiveness check is the payoff for sealing the hierarchy in the first place: it turns "did I forget to handle a case?" from a runtime bug into a compile-time one.

classDiagram class PaymentResult { <> } class Approved { <> +confirmationCode: String } class Declined { <> +reason: String } class Pending { <> } PaymentResult <|.. Approved PaymentResult <|.. Declined PaymentResult <|.. Pending note for PaymentResult "when (result) {
is Approved -> ...
is Declined -> ...
Pending -> ...
} // exhaustive, no else"

enum class

An enum class declares a fixed set of named instances, each of which can carry constructor arguments, its own members, and even a constant-specific body overriding a member per-constant:

enum class Direction(val degrees: Int) {
    NORTH(0), EAST(90), SOUTH(180), WEST(270);       // note the semicolon before further members

    fun opposite(): Direction = entries[(ordinal + 2) % entries.size]
}

enum class Operation {
    PLUS {
        override fun apply(a: Int, b: Int) = a + b    // constant-specific body
    },
    TIMES {
        override fun apply(a: Int, b: Int) = a * b
    };

    abstract fun apply(a: Int, b: Int): Int
}

println(Direction.NORTH.opposite())    // SOUTH
println(Operation.PLUS.apply(2, 3))     // 5

Direction.entries (the modern replacement for the older values()) returns an immutable List of every constant in declaration order; .ordinal is its zero-based position and .name its declared identifier — both inherited automatically, exactly like Java’s Enum. An enum class cannot be sealed and does not need to be: it is already a closed set by construction, and when over one is exhaustive on the same terms as above.

See Also

Made with ♥ from BCN