What does it mean for a switch over a sealed type to be exhaustive, and how does a sealed hierarchy let the compiler verify exhaustiveness without a default arm?
answer
- permits = closed, compile-time-known subtype set
- all cases covered => exhaustive => no default
- omit default so adding a subtype breaks compile
- switch expressions must be exhaustive
- null handled separately (case null / NPE)
basics
~20 sA sealed type lists exactly which subtypes can exist. If your switch has a case for every permitted subtype, the compiler knows nothing else is possible and accepts it as complete (exhaustive) without a default branch.
solid answer
~40 sA sealed class or interface declares a closed, compile-time-known set of subtypes via its permits clause. Because that set is fixed, the compiler can prove a switch is exhaustive: if every permitted subtype is covered by a case label, no other value (besides null, handled separately) is reachable, so a default arm is unnecessary. This applies to switch expressions and pattern-matching switches that require exhaustiveness. The real win is maintainability: when you add a new permitted subtype and recompile, every non-default switch that no longer covers all cases fails to compile, pointing you at exactly the spots to update. A default arm would instead silently swallow the new subtype, hiding the gap. So you deliberately omit default to keep the compiler as a safety net.
code
java · 12 linessealed interface Shape permits Circle, Square {}
record Circle(double r) implements Shape {}
record Square(double s) implements Shape {}
static double area(Shape sh) {
return switch (sh) { // exhaustive: no default
case Circle c -> Math.PI * c.r() * c.r();
case Square sq -> sq.s() * sq.s();
};
}
// Adding `record Triangle(...) implements Shape {}` to permits
// makes this switch fail to compile until Triangle is handled.go deeper
Knows a sealed type lists its subtypes and that covering all of them lets a switch skip default.
Explains that the permits clause closes the subtype set so the compiler can prove exhaustiveness, and that this applies to switch expressions.
Articulates the deliberate omission of default as a maintainability tool, contrasts with default swallowing new cases, and mentions null handling.
Discusses the synthetic MatchException backstop, separate-compilation/binary-compatibility implications, non-sealed subtypes, and how this generalizes enum exhaustiveness into a domain-modeling discipline.
## The terms, from first principles **A `switch`** picks one branch of code based on a value. A **`switch` *expression*** (Java 14+) is a switch that *produces a value*, e.g. `int n = switch (shape) { ... };`, unlike a `switch` *statement* that just runs code. Because an expression must yield a value for *every* possible input, the compiler insists it be **exhaustive** — it must cover every input — otherwise some input would leave the expression with no value to return. **A sealed type** (finalized in Java 17) is a class or interface marked `sealed` with a `permits` clause listing exactly which types may directly extend or implement it. Example: `sealed interface Shape permits Circle, Square, Triangle {}`. No other type — anywhere, even in another file — can be a direct subtype of `Shape`. The set of subtypes is therefore **closed** and **known at compile time**. **Exhaustiveness** means: the set of `case` labels covers every value the selector could take. For an `int` selector that is practically impossible (4 billion values), so you need `default`. But for a sealed type, the universe of values is just "instances of the permitted subtypes" (plus `null`). The compiler enumerates the permitted subtypes and checks each is matched by some case. ## Why a sealed hierarchy enables it Before sealed types, any class could be subclassed by anyone, so the compiler could never know it had seen *all* possible subtypes — it had to assume an unknown subtype might appear at runtime, forcing a `default`. `sealed` removes that uncertainty: the compiler reads the `permits` clause, sees the *complete* list, and if your switch has a case for each one, it concludes no other type is reachable. Exhaustiveness is proven, and **no `default` is required**. ```java 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) { // no default needed case Circle c -> Math.PI * c.r() * c.r(); case Square sq -> sq.s() * sq.s(); }; } ``` ## Why omit `default` on purpose This is the key engineering point. Suppose you add `record Triangle(...) implements Shape {}` to the `permits` list. Every switch that lacked a `Triangle` case now **fails to compile** with "the switch statement/expression does not cover all possible input values." The compiler hands you a to-do list of every place to update. Had you written a catch-all `default -> ...`, the code would still compile and the new `Triangle` would silently fall into `default` — a latent bug. So you trade the convenience of `default` for a compile-time guarantee that the hierarchy and its consumers stay in sync. ## Details and edge cases - **`null`:** by default `switch` throws `NullPointerException` on a null selector. Pattern switches may add `case null` to handle it; exhaustiveness checking treats null separately. Without `case null`, an exhaustive switch is still considered exhaustive — null just NPEs as usual. - **Statements vs expressions:** a `switch` *expression* is *always* required to be exhaustive. A `switch` *statement* using the new pattern/null labels is *also* checked for exhaustiveness; a legacy statement (e.g. plain `int`/`String` with no patterns) is not. - **Synthetic default / `MatchException`:** when a switch is exhaustive without `default`, the compiler may insert a hidden default that throws `MatchException` to cover the gap if, at runtime, a separately-compiled subtype appears that wasn't known at compile time (binary incompatibility). You never write this; it is a safety backstop. - **`non-sealed` subtypes:** a permitted subtype can be `non-sealed`, reopening it for further extension. Covering the `non-sealed` subtype's case still satisfies exhaustiveness because any deeper subtype *is-a* that permitted type. - **Enums** had a similar (weaker) story: a switch over an enum covering all constants in an expression is exhaustive too; sealed types generalize this to arbitrary class hierarchies.
- If you add a new permitted subtype, what happens to existing switches — with and without a default arm?Without default: every non-exhaustive switch fails to compile, flagging the spots to update. With default: they keep compiling and the new subtype silently falls into default, masking the missing handling.
- Does omitting default mean null is automatically handled?No. A null selector throws NullPointerException unless you add an explicit case null (optionally case null, default). Exhaustiveness is checked over the non-null permitted subtypes.
saying these in an interview costs you the question
- Saying you should add a default to be safe — that defeats the compile-time check.
- Claiming exhaustiveness works for any class, not just sealed (or enum) types.
- Thinking a default and full case coverage are equivalent — default hides new subtypes.
- Forgetting that null still NPEs unless a case null is present.