7.7 Sealed Classes (Java 17+)
Sealed classes are a feature officially introduced in Java 17 that lets 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 at compile time.
1. Why Sealed Classes?
Previously you had only two extremes — completely forbid inheritance or allow anyone to extend.
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
// sealed class: use permits to list the allowed child classes
public sealed class Shape permits Circle, Rectangle, Triangle {
abstract double area();
}
// Child classes have three options
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 // sealed: further restricted (needs its own permits)
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 { // Triangle's only permitted child
RightTriangle(double b, double h) { super(b, h); }
}
3. Permitted Subclass Modifiers
| Modifier | Meaning |
|---|---|
final | This class is also sealed. No further extension allowed. |
sealed | This class is also sealed, but specifies its own permits list. |
non-sealed | Seal removed. Anyone can extend this class (intentional opening). |
4. Sealed Interfaces
sealed can be applied to interfaces as well as classes.
// Complete shape hierarchy using sealed interface
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 {}
Since
recordis implicitlyfinal, it works directly as an implementation of a sealed interface.
5. Synergy with Pattern Matching (Java 21)
The true power of sealed classes comes from combining them with switch pattern matching. The compiler verifies at compile time that all cases are handled!
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 {}
public class Calculator {
// sealed + switch = exhaustiveness check at compile time
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!
};
}
public static void main(String[] args) {
// (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
A pattern for expressing success/failure as types (similar to Rust's Result<T, E>).
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); }
static <T> Result<T> err(String msg, Exception e) { return new Err<>(msg, e); }
}
public class UserService {
Result<String> findUser(int id) {
if (id <= 0) return Result.err("ID must be positive.");
if (id == 999) return Result.err("User not found.");
return Result.ok("Alice"); // success
}
void run() {
switch (findUser(1)) {
case Result.Ok<String>(var name) -> System.out.println("Found: " + name);
case Result.Err<String>(var msg, var e) -> System.out.println("Failed: " + msg);
}
}
}
Sealed class use cases:
- Algebraic Data Types (ADT): Express states or commands as types (e.g.,
sealed interface Command permits Save, Delete, Update) - API design: Restrict which types can be implemented in a public library
- Domain modeling: Represent all possible valid business states in code
Note: Sealed classes and their permits targets must be in the same package (or same module).