A switch over a sealed hierarchy that handles every permitted subtype is provably exhaustive at compile time. The compiler accepts it without a default, and — the part interviewers actually care about — when you later add a new permitted subtype, every non-exhaustive switch in your codebase turns into a compile error. The bug that used to ship to production is now caught before you hit save.
The setup
public sealed interface Shape permits Circle, Square, Triangle {}
public record Circle(double radius) implements Shape {}
public record Square(double side) implements Shape {}
public record Triangle(double base, double height) implements Shape {}
Exhaustive switch — no default needed
double area(Shape s) {
return switch (s) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Square sq -> sq.side() * sq.side();
case Triangle t -> 0.5 * t.base() * t.height();
};
}
The compiler checks: are all permitted subtypes of Shape covered? Yes. The switch compiles. No default -> throw new IllegalStateException() needed — that branch is provably unreachable.
Why this is a big deal
Imagine you add a fourth shape:
public sealed interface Shape permits Circle, Square, Triangle, Pentagon {}
public record Pentagon(double side) implements Shape {}
Now go run a build. Every switch like the one above produces:
error: the switch expression does not cover all possible input values
return switch (s) {
^
Pentagon is not covered
You find — at compile time, in the IDE, before any test runs — every place in the codebase that dispatches on Shape and forgot the new case. In a 200,000-line codebase, that's the difference between a clean refactor and a production incident two weeks later when someone uploads a pentagon and your stats dashboard silently undercounts area.
Compare to the pre-Java-17 equivalent
double area(Shape s) {
if (s instanceof Circle c) return Math.PI * c.radius() * c.radius();
else if (s instanceof Square sq) return sq.side() * sq.side();
else if (s instanceof Triangle t) return 0.5 * t.base() * t.height();
else throw new IllegalStateException("unknown shape: " + s);
}
Three pain points:
- The compiler can't prove exhaustiveness — that
throwis necessary because the compiler has no idea what else might implementShape. - Adding
Pentagoncompiles fine. TheIllegalStateExceptiononly fires at runtime, when the first pentagon flows through this code path. Maybe in production. Maybe at 3am. - The code reads as defensive plumbing — every reader has to scan past the
throwto find the actual logic.
The exhaustive switch removes all three.
Pattern matching makes it more concise
Combine sealed + records + switch with record patterns and you get destructuring as a bonus:
double area(Shape s) {
return switch (s) {
case Circle(double r) -> Math.PI * r * r;
case Square(double side) -> side * side;
case Triangle(double b, double h) -> 0.5 * b * h;
};
}
No accessor calls. The pattern destructures the record components directly into local variables.
The escape hatch — default is still allowed
If you want defensive logging on top of exhaustiveness, you can add default. The compiler then doesn't require all cases — but you usually want exhaustiveness over the convenience of a catch-all. The discipline is the value.
Why interviewers love this question
It's a litmus test for keeping up with modern Java. A senior who can articulate "sealed + exhaustive switch = compile-time safety net on refactors" understands why the language has evolved this way — it's not syntactic sugar, it's a fundamentally stronger contract. The candidate who shrugs and says "I'd add a default" hasn't seen what this protects you from.