sealed types
closed hierarchies the compiler can check exhaustively.
- chapter
- chapter 08.04
- reading time
- 6 min read
- java 17+
a sealed class or interface lists exactly who may extend it: sealed interface Shape permits Circle, Square, Rect. every permitted subclass must itself say final, sealed or non-sealed. final in java 17.
the payoff is that the compiler knows the full list of subtypes, so a switch over a sealed type can be checked for exhaustiveness: miss a case and it doesn't compile, no default needed.
the picture
you know how any class can implement your interface, so a method that takes a Shape can never be sure it has handled every kind of shape? you end up writing default -> throw new IllegalStateException("unknown shape") and hoping. sealed types replace the hope with a list.
think of a party with a guest list at the door. a normal interface is an open house: anyone can walk in, you'll never know the full list. a sealed interface is the guest list. and each guest decides about plus-ones: final means no plus-ones, sealed means "only these people", non-sealed means "bring whoever you like".
- sealed interface ... permits Circle, Square, Rect
- ├── Circlerecord: final, closed
- ├── Squarerecord: final, closed
- └── non-sealed: open again from here
- ├── RoundedRectanyone may extend Rect
- └── Window
Shape knows its three direct subtypes. Circle and Square are records (implicitly final). Rect is non-sealed, so it opens the door again: anything can extend it, but every one of those is still a Rect.the rules
every permitted subclass has to pick one of three modifiers, out loud:
- final
- no subclasses at all. records and enums are implicitly final, so they need nothing.
- sealed
- closed again, with its own
permitslist. hierarchies can be sealed several levels deep. - non-sealed
- open. anyone can extend this one. it's a deliberate escape hatch, and it's spelled with a hyphen because it's the first hyphenated keyword java ever got.
forget to pick, or try to sneak in a subclass that isn't on the list, and javac stops you:
class Circle implements Shape {}
error: sealed, non-sealed or final modifiers expected
final class Blob implements Shape {}
error: class is not allowed to extend sealed class: Shape (as it is not listed in its 'permits' clause)the permitted subclasses have to live in the same module as the sealed type (or the same package, if you're not using modules). when they're all in the same file, you can leave permits out and the compiler fills it in:
import java.util.*;
sealed interface Shape {} // no permits: inferred from this file
record Circle(double r) implements Shape {}
record Square(double side) implements Shape {}
void main() {
Shape s = new Square(2);
String d = switch (s) { // no default needed
case Circle c -> "circle " + c.r();
case Square q -> "square " + q.side();
};
IO.println(d);
IO.println(Shape.class.isSealed());
IO.println(Arrays.toString(Shape.class.getPermittedSubclasses()));
}square 2.0 true [class Main$Circle, class Main$Square]
exhaustiveness
here's the reason sealed types exist. turn cases on and off, and see what javac thinks.
- 1sealed interface Shape permits Circle, Square, Rect {}
- 2record Circle(double r) implements Shape {}
- 3record Square(double side) implements Shape {}
- 4non-sealed class Rect implements Shape {}
- 5
- 6String name(Shape shape) {
- 7 return switch (shape) {
- 8 case Circle c -> "circle";
- 9 case Square q -> "square";
- 10 };
- 11}
Rect isn't handled, and because Shape is sealed, javac knows exactly what's missing.
default. then add Triangle to the hierarchy and watch a switch that was fine stop compiling. the error text is javac 25's, verbatim.that second part is the whole point. when someone adds Triangle, every switch over Shape that doesn't handle it turns into a compile error, in every file, at once. the compiler hands you the to-do list. with a default, those switches would have compiled, and triangles would have quietly fallen into "something else".
why not an enum?
an enum is a closed set of instances: exactly one RED, one GREEN. a sealed type is a closed set of types, and each type can have as many instances as you like, each with its own data. Circle(1.0) and Circle(2.5) are both circles. that's why sealed interfaces and records go together so well: the sealed interface says "these are the shapes", the records say what each shape carries. the full pattern is data-oriented programming.
sealed ... permits A, B: only A and B may extend it. in the same file you can droppermits.- every permitted subclass says
final,sealedornon-sealed. records are already final. - a switch over a sealed type with a case per subtype is exhaustive. no default needed.
- skip the default: it hides the next subtype someone adds.
- enums close the set of instances. sealed types close the set of types.