Java Sealed Classes

Java Programming Tutorials

Java Object Oriented

Java sealed classes and interfaces let you control which types may directly extend or implement a hierarchy. They suit domain models where the valid alternatives are known, such as payment results, document states, or geometric shapes.

Sealed classes became a permanent language feature in Java 17. They do not replace ordinary inheritance. Instead, they make a deliberately closed hierarchy explicit and let the compiler reject unauthorized direct subclasses.

Declare a Sealed Class

Add the sealed modifier and list permitted direct subclasses after permits.

Example:

// Only CardPayment and CashPayment may directly extend Payment.
public sealed abstract class Payment
    permits CardPayment, CashPayment {

    private final double amount;

    protected Payment(double amount) {
        this.amount = amount;
    }

    public double amount() {
        return amount;
    }
}

The permits clause names direct subclasses only. A permitted subclass must extend the sealed type directly and must choose how its own hierarchy continues.

Choose a Permitted Subclass Modifier

Modifier Meaning When to Use
final No further subclasses are allowed The subtype is complete
sealed Only another listed set may extend it The branch needs controlled subtypes
non-sealed Any accessible type may extend it One branch must remain open

Example:

// This branch ends here.
public final class CashPayment extends Payment {
    public CashPayment(double amount) {
        super(amount);
    }
}

// This branch is open to further subclasses.
public non-sealed class CardPayment extends Payment {
    public CardPayment(double amount) {
        super(amount);
    }
}

Every permitted direct subclass must be declared final, sealed, or non-sealed. Records are implicitly final, so a record can implement a sealed interface without an extra modifier.

Use a Sealed Interface

A sealed interface works well when several unrelated implementations represent a fixed set of outcomes.

Example:

// The result hierarchy has exactly three direct implementations.
public sealed interface LoginResult
    permits Success, InvalidPassword, LockedAccount {
}

record Success(String userName) implements LoginResult { }
record InvalidPassword(int attemptsLeft) implements LoginResult { }
record LockedAccount(String supportCode) implements LoginResult { }

Package and Module Rules

Permitted subclasses must stay close to the sealed type:

  • In a named module, the sealed type and its permitted direct subclasses must belong to the same module.
  • In the unnamed module, they must belong to the same package.
  • The compiler can infer the permitted subclasses when they appear in the same source file, so the permits clause may be omitted in that case.

Example:

// The compiler infers Circle and Rectangle because they share this file.
sealed interface Shape { }

final class Circle implements Shape { }

final class Rectangle implements Shape { }

Process a Closed Hierarchy

A closed hierarchy helps the compiler reason about all alternatives. With a modern Java pattern switch, you can cover every permitted subtype without a default branch when the switch is exhaustive.

Example:

static String message(LoginResult result) {
    // Every permitted LoginResult subtype is covered.
    return switch (result) {
        case Success success ->
            "Welcome, " + success.userName();
        case InvalidPassword invalid ->
            invalid.attemptsLeft() + " attempts left";
        case LockedAccount locked ->
            "Contact support: " + locked.supportCode();
    };
}

If you later add another permitted subtype, the compiler identifies switches that no longer cover the full hierarchy. This makes sealed hierarchies useful for stable domain modeling.

Inspect Sealed Types with Reflection

The reflection API can report whether a class is sealed and list its permitted direct subclasses.

Example:

Class<Payment> type = Payment.class;

// Print whether the hierarchy is sealed.
System.out.println(type.isSealed());

// Print each permitted direct subclass.
for (Class<?> child : type.getPermittedSubclasses()) {
    System.out.println(child.getName());
}

Common Mistakes

  • Do not omit final, sealed, or non-sealed from a permitted direct subclass.
  • Do not list an indirect descendant in permits.
  • Do not spread permitted classes across unrelated modules or packages.
  • Do not seal a widely extended public class without considering binary compatibility.
  • Do not use a sealed hierarchy when third-party extensions are an intended part of the design.

Tip: Seal domain alternatives, not utility base classes. A hierarchy such as success, failure, and pending has a natural boundary; a general framework extension point usually does not.

Conclusion

Java sealed classes make inheritance boundaries explicit. Declare the allowed direct subclasses with permits, choose final, sealed, or non-sealed for every permitted branch, and follow the package or module rule. The result is a controlled model that works especially well with records and exhaustive pattern switches.



Found This Page Useful? Share It!
Get the Latest Tutorials and Updates
Join us on Telegram