Sealed classes — what problem do they solve?
MediumA sealed class or interface lists exactly which types may extend it. You keep a public type but a closed set of subtypes, so the compiler can check a switch covers them all. Every subtype states whether the hierarchy stays closed or opens again (non-sealed). Java 17+.
How it works
- Declare the set.
sealed interface Payment permits Card, Wallet, BankTransfer. Any other class that tries to implementPaymentfails to compile. - Each subtype picks a modifier.
final: nothing below it (records and enums are already final).sealed: closed again, with its ownpermitslist.non-sealed: open to anyone from here down.
- Location rule. The sealed type and every class it lists have to live together: one module, or one package when the code isn't modular. When they all sit in one source file you can drop the
permitsclause and the compiler infers it. - Why it matters.
- Before Java 17 a type was either open to everyone or
final. Package-private constructors were a hack that also hid the type's purpose. - Domain models often are closed: a payment is one of three kinds, never a fourth from a plugin.
- The compiler and pattern-matching
switchcan now prove a switch is exhaustive, with nodefault.
- Before Java 17 a type was either open to everyone or
- Runtime too. The JVM enforces the list, and
Class.isSealed()/getPermittedSubclasses()expose it to reflection.
A variant where one branch is sealed again and another is reopened:
Example
sealed interface Payment permits Card, Wallet, BankTransfer {}
record Card(String last4) implements Payment {}
record Wallet(String provider) implements Payment {}
record BankTransfer(String iban) implements Payment {}
static double feePercent(Payment p) {
return switch (p) { // exhaustive: no default needed
case Card c -> 2.9;
case Wallet w -> 2.5;
case BankTransfer b -> 0.8;
};
}Add record Voucher(String code) implements Payment {} and update permits, and feePercent stops compiling until you handle Voucher. With an open interface and a default branch, the new type would have silently taken the default fee.
Edge cases
- Anonymous and local classes can never be permitted subtypes.
- A
non-sealedsubtype reopens that branch, so switches must cover it with a type pattern for it (or adefault). - Sealing an interface doesn't stop other code from using it as a type; it only restricts who implements it.
- In the unnamed module, permitted subclasses in another package are a compile error even if they're public.
Common mistakes
- Thinking
sealedmeans "can't be subclassed". It means "can only be subclassed by these". - Adding a
defaultbranch to switches over sealed types, which throws away the exhaustiveness check. - Forgetting a permitted subtype needs one of
final,sealedornon-sealed(records getfinalfor free).
Likely follow-up
"Sealed interface or enum?" Use an enum when every case is a single fixed instance with the same fields. Use a sealed interface with records when the cases carry different data, like a card number versus an IBAN.