Working with Sealed Classes (Java 17+)
1. Creating Sealed Classes
| Modifier | Effect |
|---|---|
sealed | Restricts who can extend |
permits | Lists permitted subclasses |
| Subclass must | be final, sealed, or non-sealed |
| Same module | Permitted types must reside in same module/package |
Example: Sealed hierarchy
public sealed class Vehicle permits Car, Truck, Bike { }
public final class Car extends Vehicle { }
public non-sealed class Truck extends Vehicle { }
public sealed class Bike extends Vehicle permits MountainBike { }
2. Using permits Clause
| Rule | Detail |
|---|---|
| Optional | If all permitted types in same file |
| Required | If subclasses are in other files |
| Order | Doesn't matter |
3. Understanding Subclass Modifiers
| Modifier | Meaning |
|---|---|
final | Closes hierarchy at this leaf |
sealed | Continues with another permits |
non-sealed | Re-opens for unrestricted extension |
4. Using Sealed Interfaces
| Aspect | Detail |
|---|---|
| Implementers | Same rules: final/sealed/non-sealed |
| Records | Records are implicitly final |
| Use case | Algebraic data types (sum types) |
5. Understanding Exhaustiveness Checking
| Context | Behavior |
|---|---|
| Switch expression on sealed type | Compiler verifies all permitted types covered |
| Missing case | Compile error |
| Default | Optional (no error if exhaustive) |
6. Using Sealed Classes with Pattern Matching
Example: Exhaustive switch
sealed interface Result<T> permits Ok, Err { }
record Ok<T>(T value) implements Result<T> { }
record Err<T>(String msg) implements Result<T> { }
String render(Result<Integer> r) {
return switch (r) {
case Ok<Integer>(Integer v) -> "ok: " + v;
case Err<Integer>(String m) -> "err: " + m;
};
}
| Benefit | Detail |
|---|---|
| No default | Compiler-enforced exhaustiveness |
| Refactor-safe | Adding subtype → compile error |
| Smart cast | Pattern variable typed |
7. Understanding Design Benefits
| Benefit | Use |
|---|---|
| Closed sets | Domain modeling (states, events) |
| Type safety | Visitor pattern alternative |
| Reflection | Tools can list permitted types |
| vs enum | Each variant can have its own data |
8. Combining Sealed Classes and Records
Example: ADT
sealed interface Expr permits Num, Add, Mul { }
record Num(double v) implements Expr { }
record Add(Expr l, Expr r) implements Expr { }
record Mul(Expr l, Expr r) implements Expr { }
double eval(Expr e) {
return switch (e) {
case Num(double v) -> v;
case Add(Expr l, Expr r) -> eval(l) + eval(r);
case Mul(Expr l, Expr r) -> eval(l) * eval(r);
};
}
| Pattern | Result |
|---|---|
| Sealed interface + records | Sum-of-products / ADT |
| Record patterns | Destructure inline |
9. Understanding Reflection with Sealed Classes
| API | Returns |
|---|---|
cls.isSealed() | boolean |
cls.getPermittedSubclasses() | Class<?>[] |
10. Using Sealed Classes for Domain Modeling
| Domain | Pattern |
|---|---|
| Order state | sealed interface OrderState permits Pending, Paid, Shipped, Cancelled |
| Payment method | Card / Bank / Crypto record types |
| Result | Ok / Err sum type |
| AST nodes | Lit / BinaryOp / UnaryOp |