Sealed Classes
A sealed class (a standard feature since Java 17) restricts exactly which other classes or interfaces are allowed to extend or implement it. Instead of any class in your codebase being free to extend a public class, a sealed class names, up front, the complete, closed list of permitted subtypes.
A sealed hierarchy
sealed interface Shape permits Circle, Square, Triangle {}
final class Circle implements Shape {
double radius;
Circle(double radius) { this.radius = radius; }
}
final class Square implements Shape {
double side;
Square(double side) { this.side = side; }
}
final class Triangle implements Shape {
double base, height;
Triangle(double base, double height) {
this.base = base;
this.height = height;
}
}Shape explicitly lists its only allowed implementers with permits Circle, Square, Triangle. No other class — anywhere, in any package — is permitted to implement Shape. The compiler enforces this list at compile time.
The permits clause
permits lists every class or interface allowed to directly extend or implement the sealed type. If every permitted subtype is declared in the same file as the sealed type, you can actually omit permits entirely — the compiler infers it from the file's contents. Once listed (or inferred), that list is closed: attempting to add a new subclass anywhere else in the codebase is a compile-time error.
Subclasses must declare their own openness
Every class permitted by a sealed type must itself pick exactly one of three modifiers, so there's never any ambiguity about what happens further down the hierarchy:
Modifier | Meaning |
|---|---|
| Closes the hierarchy here — cannot be extended further |
| Continues to restrict its own subtypes with its own |
| Reopens the hierarchy — any class is free to extend this one |
In the example above, Circle, Square, and Triangle are all declared final, so the Shape hierarchy is completely closed after one level — there can never be a fourth kind of Shape.
Why this matters: exhaustive pattern matching
Because the compiler knows the complete set of possible subtypes of a sealed type, a switch expression over that type can be checked for exhaustiveness — the compiler can confirm every case is covered, with no default branch needed at all.
An exhaustive switch — no default needed
double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius * c.radius;
case Square s -> s.side * s.side;
case Triangle t -> 0.5 * t.base * t.height;
// no 'default' required — the compiler knows these are ALL the Shapes
};
}sealedrestricts which classes/interfaces may extend or implement a type.permitsnames the closed, complete list of direct subtypes.Every permitted subtype must be
final,sealed, ornon-sealed.A closed hierarchy enables the compiler to verify
switchexhaustiveness with nodefaultcase.