7.7 Sealed Classes (Java 17+)
Sealed classes (introduced officially in Java 17) let you explicitly restrict which classes can extend or implement a given class or interface. This gives you full control over inheritance hierarchies and lets the compiler verify exhaustiveness.
1. Why Sealed Classes?
Old options:
- final class Shape → Nobody can extend (too restrictive)
- class Shape → Anyone can extend (too open)
- sealed class Shape → Only specified classes can extend (just right!) ← Java 17
2. Basic Syntax
public sealed class Shape permits Circle, Rectangle, Triangle {
abstract double area();
}
final class Circle extends Shape { // final: no further extension
private final double radius;
Circle(double r) { this.radius = r; }
@Override double area() { return Math.PI * radius * radius; }
}
non-sealed class Rectangle extends Shape { // non-sealed: freely extensible again
protected double width, height;
Rectangle(double w, double h) { this.width = w; this.height = h; }
@Override double area() { return width * height; }
}
sealed class Triangle extends Shape permits RightTriangle {
protected double base, height;
Triangle(double b, double h) { this.base = b; this.height = h; }
@Override double area() { return 0.5 * base * height; }
}
final class RightTriangle extends Triangle {
RightTriangle(double b, double h) { super(b, h); }
}
3. Permitted Subclass Modifiers
| Modifier | Meaning |
|---|---|
final | Cannot be extended further |
sealed | Also sealed; must specify its own permits list |
non-sealed | Seal removed; anyone can extend it |
4. Sealed Interfaces
public sealed interface Shape permits Circle, Rectangle, Triangle {}
public record Circle(double radius) implements Shape {}
public record Rectangle(double width, double height) implements Shape {}
public record Triangle(double base, double height) implements Shape {}
5. Synergy with Pattern Matching (Java 21)
The real power of sealed classes comes from combining them with switch pattern matching. The compiler verifies that all cases are handled at compile time!
public sealed interface Expr permits Num, Add, Mul {}
record Num(int value) implements Expr {}
record Add(Expr left, Expr right) implements Expr {}
record Mul(Expr left, Expr right) implements Expr {}
static int eval(Expr expr) {
return switch (expr) {
case Num(int v) -> v;
case Add(var l, var r) -> eval(l) + eval(r);
case Mul(var l, var r) -> eval(l) * eval(r);
// No default needed! The compiler knows all subtypes via sealed
// Adding a new Expr subtype causes a compile error here → prevents bugs!
};
}
// (2 + 3) * 4
Expr expr = new Mul(new Add(new Num(2), new Num(3)), new Num(4));
System.out.println(eval(expr)); // 20
6. Real-World Usage: Result Type
public sealed interface Result<T> permits Result.Ok, Result.Err {
record Ok<T>(T value) implements Result<T> {}
record Err<T>(String message, Exception cause) implements Result<T> {}
static <T> Result<T> ok(T value) { return new Ok<>(value); }
static <T> Result<T> err(String msg) { return new Err<>(msg, null); }
}
Pro Tip
Sealed class use cases:
- Algebraic Data Types (ADT): Express states or commands as types
- API design: Restrict which types can be implemented in a library
- Domain modeling: Represent all possible business states in code
Note: Sealed classes and their permits targets must be in the same package (or same module).