Sealed hierarchies + exhaustive switch in Java
Algebraic data types with no default branch needed.
A closed list of subtypes
A sealed interface or class controls exactly which types may extend it: sealed interface Shape permits Circle, Square {}. Nobody else can implement Shape, so the compiler knows the complete list of possibilities.
Every subtype declares its openness
Each direct subtype must be **final (no further subclasses), sealed (only its listed subclasses) or non-sealed** (re-opened: anyone may extend it). Records and enums are implicitly final, so they qualify automatically.
sealed interface V permits Car, Bus, Bike {}
final class Car implements V {}
sealed class Bus implements V
permits MiniBus {}
final class MiniBus extends Bus {}
non-sealed class Bike implements V {}Exhaustive, no default
Because the compiler knows every subtype, a switch that handles them all is exhaustive: no default branch needed.
sealed interface Shape permits Circle, Square {}
record Circle(double r) implements Shape {}
record Square(double s) implements Shape {}
double area(Shape sh) {
return switch (sh) {
case Circle(var r) -> Math.PI * r * r;
case Square(var s) -> s * s;
};
}Your turn
No default branch. What does this print?
sealed interface Light permits Red, Green {}
record Red() implements Light {}
record Green(int secs) implements Light {}
void main() {
Light l = new Green(30);
System.out.println(switch (l) {
case Red r -> "stop";
case Green g -> "go " + g.secs();
});
}go 30Compile error: missing defaultstop
Show the answer
go 30. Light is sealed to Red and Green, both are handled, so the switch is exhaustive without a default. The value is a Green with 30 seconds.
Missing a case? It won't compile
Leave out a permitted subtype and the switch isn't exhaustive: compile error, even if the value at runtime happens to be a type you did handle. And beware default: it silences this check, so a new subtype slips into it quietly. With sealed types, prefer no default.
// Pet permits Dog, Cat
Pet p = new Dog();
String s = switch (p) {
case Dog d -> "dog";
}; // compile error: Cat not coveredSix broken builds
Your team adds record Triangle(...) implements Shape. The build now fails in 6 places. Why is that good news?
Think about it, then reveal the answer
Each failure is a switch over Shape that is no longer exhaustive. The compiler hands you the exact list of places that must handle Triangle, turning a forgotten case into a compile error instead of a production bug.
Closed domains
Payment states, API responses, shapes in a drawing app, commands in a protocol: whenever the set of variants is known, seal it. Library authors also seal types to control who may extend their API, without making everything final.
Key takeaways
- sealed ... permits A, B names the allowed subtypes
- Subtypes must be final, sealed, or non-sealed (records are final)
- A switch covering every subtype needs no default
- New subtype → incomplete switches fail to compile (good!)
The JDK seals its own types: java.lang.constant.ConstantDesc is a sealed interface whose permitted subtypes include String, Integer and ClassDesc.
Practice questions
What does this print?
sealed interface Pet permits Dog, Cat {}
record Dog(String name) implements Pet {}
record Cat(int lives) implements Pet {}
void main() {
Pet p = new Cat(9);
System.out.println(switch (p) {
case Dog d -> "woof " + d.name();
case Cat c -> "meow x" + c.lives();
});
}- meow x9
- Compile error: missing default
- woof null
- Throws MatchException
Check your answer
meow x9. Pet is sealed to Dog and Cat, and both are handled, so the switch is exhaustive without a default.
What does this print?
sealed interface Pet permits Dog, Cat {}
record Dog() implements Pet {}
record Cat() implements Pet {}
void main() {
Pet p = new Dog();
String s = switch (p) {
case Dog d -> "dog";
};
System.out.println(s);
}- Compile error
- dog
- Throws MatchException
- null
Check your answer
Compile error. Cat is a permitted Pet with no matching case, so the switch isn't exhaustive. The compiler refuses it even though p happens to be a Dog.