Sealed Classes in Java
Introduction
In object-oriented programming, inheritance is a powerful tool for code reuse and modeling real-world hierarchies. However, unrestricted inheritance can lead to fragile architectures. Prior to Java 15, if you wanted to restrict which classes could extend your class, your options were limited: make the class final (no one can extend it), or use package-private constructors (only classes in the same package can extend it).
Introduced permanently in Java 17, Sealed Classes and Interfaces provide a declarative way to restrict class hierarchies. They allow a class or interface to explicitly define exactly which other classes are permitted to extend or implement it.
This feature gives developers precise control over inheritance, making domain modeling more secure and enabling exhaustive Pattern Matching.
Core Concept: How Sealed Classes Work
A sealed class or interface uses the sealed modifier and explicitly lists its permitted subclasses using the permits clause.
The Rules of Sealed Hierarchies
- Permitted Subclasses: A sealed class must declare subclasses using
permits, unless the subclasses are defined in the same file. - Same Module/Package: Permitted subclasses must belong to the same module (if using Java modules) or the same package (if unnamed module).
- Subclass Modifiers: Every permitted subclass must explicitly define its own inheritance boundaries using one of three modifiers:
final: Cannot be extended further.sealed: Can be extended, but only by its own explicitly permitted subclasses.non-sealed: Breaks the seal, allowing any class to extend it (reverting to traditional open inheritance).
Code Examples
1. Beginner Example: Modeling a Domain
Imagine you are building a payment processing system. A payment can only be a Credit Card, a Bank Transfer, or Cash. You don't want external developers adding a CryptoPayment class without your knowledge.
// 1. Define the sealed interface
public sealed interface PaymentMethod
permits CreditCard, BankTransfer, Cash {
}
// 2. Define the permitted implementations
// final: cannot be extended
public final class CreditCard implements PaymentMethod {
private final String cardNumber;
public CreditCard(String cardNumber) { this.cardNumber = cardNumber; }
}
// final: cannot be extended
public final class BankTransfer implements PaymentMethod {
private final String routingNumber;
public BankTransfer(String routingNumber) { this.routingNumber = routingNumber; }
}
// non-sealed: open for anyone to extend further
public non-sealed class Cash implements PaymentMethod {
private final double amount;
public Cash(double amount) { this.amount = amount; }
}
2. Intermediate Example: Omitted Permits Clause
If the permitted subclasses are tiny, you can define them in the exact same source file as the sealed class. When you do this, the Java compiler infers the permits clause automatically.
// AuthResult.java
public sealed class AuthResult {
// Classes defined in the same file are implicitly permitted.
// No need for 'permits Success, Failure'
}
final class Success extends AuthResult {
private final String token;
public Success(String token) { this.token = token; }
public String getToken() { return token; }
}
final class Failure extends AuthResult {
private final String reason;
public Failure(String reason) { this.reason = reason; }
public String getReason() { return reason; }
}
3. Advanced Example: Synergy with Pattern Matching (Java 21+)
The true power of Sealed Classes is unlocked when combined with Record Patterns and switch expressions. Because the compiler knows exactly all possible subclasses of a sealed class, it can enforce exhaustiveness in a switch.
public class PaymentProcessor {
// Notice there is NO 'default' branch needed!
// The compiler knows PaymentMethod can only be CC, Bank, or Cash.
public void process(PaymentMethod payment) {
switch (payment) {
case CreditCard cc ->
System.out.println("Processing credit card: " + cc);
case BankTransfer bank ->
System.out.println("Processing bank transfer: " + bank);
case Cash cash ->
System.out.println("Processing physical cash: " + cash);
}
}
}
If someone later adds Crypto to the PaymentMethod permits clause but forgets to update this switch statement, the code will fail to compile. This is a massive win for maintainability and refactoring.
Common Mistakes Beginners Make
1. Forgetting Subclass Modifiers
The Mistake: Creating a permitted subclass without declaring it final, sealed, or non-sealed.
The Fix: The compiler requires you to explicitly state the inheritance rules for the next generation. Choose one of the three modifiers.
2. Cross-Package Permitting (in Unnamed Modules)
The Mistake: Trying to permit a class located in a different package when not using the Java Module System (JPMS).
The Fix: Ensure all permitted subclasses reside in the same package as the sealed class unless you are explicitly defining a module-info.java where they exist in the same module.
Summary
- Sealed Classes (
sealed,permits) provide fine-grained control over inheritance hierarchies. - Subclasses must explicitly opt into their future inheritance state using
final,sealed, ornon-sealed. - They enable highly secure domain modeling (Algebraic Data Types).
- They integrate beautifully with Switch Pattern Matching, allowing the compiler to guarantee exhaustive checks and eliminating the need for brittle
defaultblocks.