Java inheritance is open by default. If a class is not final, another class can normally extend it. That flexibility is useful, but sometimes a domain model should support only a known set of subtypes.

Java sealed classes provide the middle ground between unrestricted inheritance and a completely closed final class. A sealed class or interface explicitly controls which types may directly extend or implement it.

Sealed classes became a permanent Java language feature in Java 17 through JEP 409. They have become even more useful as Java's pattern-matching features have matured, particularly pattern matching for switch, finalized in Java 21.

Why use a sealed class?

Imagine an application that processes three kinds of financial transactions:

  • StockTransaction
  • BondTransaction
  • CryptoTransaction

The application wants these to be the only direct implementations of its transaction model. A normal superclass cannot enforce that restriction, while a final superclass would prevent all inheritance.

A sealed hierarchy expresses the intended model directly in the Java type system.

public sealed interface Transaction
        permits StockTransaction,
                BondTransaction,
                CryptoTransaction {
}

Each permitted implementation must then explicitly state what happens to inheritance below it.

public final class StockTransaction
        implements Transaction {
}

public final class BondTransaction
        implements Transaction {
}

public final class CryptoTransaction
        implements Transaction {
}

Because all three implementations are final, the hierarchy is closed at that level. Another developer cannot introduce a fourth implementation of Transaction without changing the sealed hierarchy.

The sealed, permits and non-sealed keywords

Three modifiers are central to sealed hierarchies:

  • sealed means inheritance is restricted.
  • final means inheritance stops at that subtype.
  • non-sealed deliberately reopens inheritance below a permitted subtype.

The permits clause names the types allowed to directly extend a sealed class or implement a sealed interface.

public sealed interface Expr
        permits ConstantExpr,
                PlusExpr,
                TimesExpr,
                NegExpr {
}

public final class ConstantExpr
        implements Expr {
}

public final class PlusExpr
        implements Expr {
}

public non-sealed class TimesExpr
        implements Expr {
}

public final class NegExpr
        implements Expr {
}

In this example, only the four listed types may directly implement Expr. However, because TimesExpr is non-sealed, other classes may extend TimesExpr.

The permits clause can sometimes be omitted

The permits clause is not always required in source code. If the permitted direct subclasses are declared in the same compilation unit as the sealed type, the compiler can infer them.

sealed interface Result {
}

final class Success implements Result {
}

final class Failure implements Result {
}

Here, the compiler infers that Success and Failure are the permitted direct implementations of Result.

Sealed class rules

Java enforces several important rules for sealed hierarchies:

  • Every permitted subtype must directly extend or implement the sealed type.
  • Every permitted class must declare itself final, sealed or non-sealed.
  • If the sealed type is in a named module, its permitted direct subclasses must be in the same module.
  • If the sealed type is in an unnamed module, its permitted direct subclasses must be in the same package.
  • A permitted subtype may itself be abstract, provided it follows the sealed hierarchy rules.
  • A type that is not permitted cannot directly extend or implement the sealed type.

For example, the following class fails to compile because DivideExpr is not a permitted implementation of Expr:

public final class DivideExpr
        implements Expr {
}

The compiler rejects the declaration because DivideExpr is not listed as a permitted subtype.

Sealed classes and pattern matching for switch

One of the most useful modern applications of sealed types is exhaustive pattern matching. Because the compiler knows the permitted hierarchy, it can determine whether a switch handles every possible subtype.

Consider a sealed expression hierarchy implemented with records:

sealed interface Expression
        permits Constant, Add, Multiply {
}

record Constant(double value)
        implements Expression {
}

record Add(Expression left, Expression right)
        implements Expression {
}

record Multiply(Expression left, Expression right)
        implements Expression {
}

Modern Java can use type patterns in a switch expression to process the hierarchy:

static double evaluate(Expression expression) {
    return switch (expression) {
        case Constant c ->
            c.value();

        case Add a ->
            evaluate(a.left()) +
            evaluate(a.right());

        case Multiply m ->
            evaluate(m.left()) *
            evaluate(m.right());
    };
}

No default branch is required because the compiler can determine that all permitted implementations of Expression are covered.

This is an important advantage over an ordinary open interface. If another permitted subtype is later added to the sealed hierarchy, an exhaustive switch that no longer covers every case can be identified at compile time.

Sealed classes work especially well with records

Sealed interfaces and records are a natural combination for modeling a fixed set of data alternatives. The sealed interface defines the complete family of permitted types, while records provide concise immutable implementations.

public sealed interface Payment
        permits CashPayment,
                CardPayment,
                CryptoPayment {
}

public record CashPayment(double amount)
        implements Payment {
}

public record CardPayment(
        double amount,
        String lastFourDigits)
        implements Payment {
}

public record CryptoPayment(
        double amount,
        String walletAddress)
        implements Payment {
}

This style is useful for domain models, syntax trees, command results, messages, state machines and other situations where the set of alternatives is deliberately constrained.

When should a subtype be non-sealed?

Use non-sealed when the parent hierarchy should control its immediate branches but one branch should remain extensible.

public sealed interface Account
        permits SavingsAccount,
                BusinessAccount {
}

public final class SavingsAccount
        implements Account {
}

public non-sealed class BusinessAccount
        implements Account {
}

The Account interface controls its direct implementations, but the BusinessAccount branch is intentionally open to further specialization.

Benefits of Java sealed classes

Controlled inheritance. Sealed types make the intended boundaries of a hierarchy part of the source code and enforce those boundaries at compile time.

Exhaustive pattern matching. A closed hierarchy works naturally with pattern matching for switch, allowing the compiler to verify that all permitted cases are handled.

Better domain modeling. Sealed types clearly express domains that contain a known set of alternatives, such as payment types, expression nodes, command results or application states.

Safer API design. Library authors can expose an inheritance hierarchy without allowing arbitrary third-party implementations of its root type.

Selective extensibility. A permitted subtype can be final, remain sealed, or use non-sealed to reopen inheritance for one specific branch.

Sealed class vs. final class

Modifier Inheritance behavior
final No subclasses are allowed.
sealed Only permitted direct subclasses are allowed.
non-sealed Normal open inheritance resumes for that branch.

A final class closes inheritance completely. A sealed type provides more precise control by allowing the developer to define exactly where inheritance may continue and where it must stop.

Sealed classes and interfaces are now an important part of modern Java's type system. Combined with records and pattern matching for switch, they make it possible to model closed domains concisely while giving the compiler much more information about the possible shapes of an application's data.

A N M Bazlur Rahman is a Java Champion and staff software developer at DNAStack. He is also founder and moderator of the Java User Group in Bangladesh.