How does exhaustiveness work in a pattern switch, and how do sealed types let you write a switch with no default?
answer
- Pattern switch / switch expression must be exhaustive
- Open type → default; sealed type → cover all permits, no default
- No default = new subtype breaks compile (good)
- MatchException = synthetic runtime fallback
- null not auto-covered; needs case null
basics
~20 sA pattern switch must handle every possible value (be exhaustive). If you switch over a sealed type, the compiler knows all its allowed subtypes, so once you cover each one you don't need a default — and if someone adds a new subtype later, the switch stops compiling until you handle it.
solid answer
~50 sExhaustiveness means a pattern switch (and every switch expression) must cover all possible inputs, checked by the compiler. For an open type like `Object` you make it exhaustive with a `default`. For a **sealed** type — one that explicitly lists its permitted subclasses — the compiler knows the full set of possibilities, so a switch that has a case for each permitted subtype is exhaustive **without** a default. This is powerful for maintainability: if you later add a new permitted subtype, the switch no longer covers all cases and the compilation fails, pointing you to every switch that must be updated. With a catch-all `default` you would lose that safety because the new type silently falls into default. The compiler also inserts a hidden fallback that throws `MatchException` if, due to separate compilation, an unexpected value appears at runtime. Sealed types + records + pattern switch are the core of Java's data-oriented programming style.
code
java · 12 linessealed interface Shape permits Circle, Square, Triangle {}
record Circle(double r) implements Shape {}
record Square(double s) implements Shape {}
record Triangle(double b, double h) implements Shape {}
static double area(Shape shape) {
return switch (shape) { // exhaustive, no default needed
case Circle c -> Math.PI * c.r() * c.r();
case Square sq -> sq.s() * sq.s();
case Triangle t -> 0.5 * t.b() * t.h();
};
}go deeper
Understands a switch expression must cover all cases or have a default.
Knows sealed types enable a default-free exhaustive switch and that missing a case is a compile error.
Explains why omitting default on sealed types is desirable, MatchException, and that null isn't auto-covered.
Designs sealed hierarchies + records for data-oriented programming, weighs exhaustiveness as an architectural invariant, and reasons about separate-compilation and API evolution implications.
## What exhaustiveness means **Exhaustiveness** is the property that a switch handles **every possible value** the selector could take. Java requires it for: - every **switch expression** (a switch that produces a value, e.g. `var x = switch(...) {...};`), and - every **switch statement that uses patterns**. The compiler verifies it; a non-exhaustive switch is a compile error. The reason is safety: an expression must always yield a value, so there can be no unhandled input. ## Making an open type exhaustive For a selector typed as something open like `Object`, there are infinitely many possible runtime types, so you cannot enumerate them. You make the switch exhaustive with a **`default`** branch (or `case null, default`), which catches everything not otherwise matched. ## Sealed types: a closed set of possibilities A **sealed** type (class or interface) explicitly restricts which other classes may extend or implement it: ```java sealed interface Shape permits Circle, Square, Triangle {} record Circle(double r) implements Shape {} record Square(double s) implements Shape {} record Triangle(double b, double h) implements Shape {} ``` Because `Shape` *permits* exactly `Circle`, `Square`, and `Triangle`, the compiler knows the **complete** set of subtypes. (A `record` is a compact immutable data class; here they're the variants.) ## Exhaustive switch with no default Now a switch over `Shape` is exhaustive once it covers all three permitted subtypes — **no default needed**: ```java double area(Shape s) { return switch (s) { case Circle c -> Math.PI * c.r() * c.r(); case Square sq -> sq.s() * sq.s(); case Triangle t -> 0.5 * t.b() * t.h(); // no default required — all permitted types covered }; } ``` ## Why omitting default is a feature, not laziness If you later add a fourth variant: ```java sealed interface Shape permits Circle, Square, Triangle, Hexagon {} ``` the `area` switch is **no longer exhaustive**, so it **fails to compile** until you add a `case Hexagon`. The compiler thus points you at *every* switch that must be updated — a refactoring safety net. Had you written a `default -> ...`, the new `Hexagon` would silently fall into `default`, hiding the omission and likely producing a bug. So when switching over a sealed type, **prefer no default** to keep the exhaustiveness check meaningful. (Use a default only when you genuinely want a deliberate catch-all.) ## MatchException and separate compilation Classes can be compiled separately, so at runtime a sealed hierarchy might have changed since the switch was compiled (e.g. a recompiled `Shape` with a new subtype, but the switch not recompiled). To stay safe, the compiler inserts a synthetic fallback that throws **`MatchException`** if no case matches at runtime. You normally never see it; it guarantees the switch can't silently "fall off the end." ## null and exhaustiveness Exhaustiveness over a reference type does not, by itself, include `null` — null still throws NPE unless you add `case null`. So "covers all subtypes" means all non-null values; null is handled separately (see the null question). ## Deriving an answer Exhaustive = handles every input, compiler-enforced for expressions/pattern switches. Open type → need default. Sealed type → compiler knows all permitted subtypes, so covering each makes it exhaustive without default; omitting default turns "added a new variant" into a compile error instead of a silent bug. A hidden MatchException guards the separate-compilation edge case.
- Why is it often better to omit `default` when switching over a sealed type?Without default, adding a new permitted subtype makes existing switches non-exhaustive, so they fail to compile and point you to every place needing an update. A default would silently absorb the new type and hide the bug.
- When can a MatchException be thrown?It is the compiler-inserted fallback for a pattern switch with no explicit default. It triggers if, due to separate compilation, a runtime value matches none of the cases — e.g. a sealed hierarchy changed after the switch was compiled.
saying these in an interview costs you the question
- Adding `default` to a sealed-type switch out of habit, defeating the exhaustiveness check
- Thinking exhaustiveness includes null automatically
- Believing you must list a default for sealed types
- Not knowing MatchException can occur and assuming a switch can silently fall through