How does a sealed type hierarchy let a switch expression be exhaustive without a default arm, and why is omitting default sometimes preferable?
answer
- sealed = closed set => compiler enumerates subtypes
- switch expression MUST be exhaustive
- cover every permitted subtype (unguarded) => no default needed
- omit default => new subtype = compile error (a checklist)
- vs visitor: same total coverage, less boilerplate
- synthetic default throws MatchException on version skew
basics
~20 sA sealed type lists all its permitted subtypes, so the compiler knows the complete set of possibilities. If your switch has an arm for each one, it covers everything and you can skip default. Omitting default means adding a new subtype later causes a compile error, reminding you to handle it.
solid answer
~50 sA sealed interface or class declares a closed set of permitted subtypes via permits. Because that set is fixed and known at compile time, a switch over the sealed type is exhaustive as soon as every permitted subtype has a covering (unguarded) arm — the compiler accepts it with no default. Switch expressions require exhaustiveness, so this is what makes them usable over closed hierarchies. The real payoff is omitting default deliberately: if you later add a new permitted subtype, every switch that lacked default now fails to compile, pointing you at exactly the places that must handle the new case. A default arm would instead silently absorb the new type, hiding the omission until runtime. This compiler-enforced completeness is the core of data-oriented programming in Java: sealed data + records + exhaustive switches replace the visitor pattern with far less ceremony while keeping the same total-coverage guarantee.
go deeper
Knows that covering all of a sealed type's subtypes means you can skip default, and that switch expressions must cover everything.
Explains the compiler enumerates permitted subtypes and that an unguarded arm per subtype makes the switch exhaustive without default.
Deliberately omits default to get compile errors when the hierarchy grows, compares it to the visitor pattern, and knows guarded arms don't count toward coverage.
Treats exhaustive sealed switches as an architectural choice for evolvable domain models, weighs visitor vs pattern-switch extensibility (adding operations vs adding types), and accounts for MatchException under separate compilation in API/versioning strategy.
## Sealed types in one paragraph A **sealed** type (class or interface, Java 17+) restricts who may extend or implement it. You write `sealed interface Shape permits Circle, Square, Triangle {}`. Only the listed types — the **permitted subtypes** — may implement `Shape`, and each must itself be `final`, `sealed`, or `non-sealed`. The result is a **closed hierarchy**: the compiler knows the *complete, finite* set of direct subtypes. ## Exhaustiveness **Exhaustiveness** means: for every possible value of the selector type, *some* arm matches. A **switch expression** (one that produces a value) **must** be exhaustive — otherwise some input would have no value to return, which the language forbids at compile time. For an open type like `Object` you can only be exhaustive by writing `default` (or `case Object o`). But for a **sealed** selector, the compiler enumerates the permitted subtypes; once each has a covering unguarded arm, the switch is **provably exhaustive without `default`**: ```java sealed interface Shape permits Circle, Square {} record Circle(double r) implements Shape {} record Square(double s) implements Shape {} double area(Shape shape) { return switch (shape) { // switch EXPRESSION, must be exhaustive case Circle c -> Math.PI * c.r() * c.r(); case Square s -> s.s() * s.s(); // no default — compiler proves all Shapes are covered }; } ``` ### What counts toward coverage - An **unguarded** type pattern for each permitted subtype. (Guarded arms do *not* count — see guards.) - The compiler also requires handling of `null` only if a pattern label is present and you want null matched; otherwise null throws NPE and does not affect the exhaustiveness proof. ## Why omit default on purpose This is the senior insight. Suppose you later add `record Triangle(...) implements Shape {}` to the `permits` clause. Now: - A switch **with** `default` keeps compiling — the new `Triangle` silently falls into `default`. The bug surfaces only at runtime (wrong area, or a thrown exception you wrote in default). - A switch **without** `default` **fails to compile** at every site that didn't add a `Triangle` arm. The compiler hands you a checklist of exactly what to update. So **omitting `default` turns "I forgot a case" from a runtime surprise into a compile error.** This is the same total-coverage guarantee the **visitor pattern** gave (the visitor interface forces an `accept` method per type), but with far less boilerplate. ## Synthetic default and MatchException Even a `default`-less exhaustive switch isn't *truly* impossible to miss at runtime: if the sealed hierarchy is recompiled separately (a new subtype added but the switch's class **not** recompiled — a binary mismatch), the JVM may hit a value with no matching arm. The compiler inserts a hidden **synthetic default** that throws **`MatchException`** in that case, so you fail loudly rather than silently. This is rare and indicates a build/version skew. ## When you DO want default - The selector is an **open** type (not sealed), so you can't enumerate it. - You genuinely want a catch-all behavior that should *not* break when the domain grows. - You're writing a switch *statement* (not expression) that intentionally ignores some cases. ## Terms recap - **Sealed / permits**: closed set of subtypes. - **Exhaustiveness**: every input handled (mandatory for switch expressions). - **Synthetic default / MatchException**: the safety net for separate-compilation skew. - **Data-oriented programming**: model with sealed + records, process with exhaustive switches.
- Why is omitting default on a sealed switch often safer than including one?Without default, adding a new permitted subtype makes every incomplete switch fail to compile, giving you a precise list of sites to update. With default, the new subtype is silently absorbed and the gap surfaces only at runtime.
- If a switch over a sealed type compiles as exhaustive with no default, can it ever fail at runtime?Yes, rarely: if the sealed hierarchy is recompiled with a new subtype but the switch's class is not recompiled, the JVM may meet an unmatched value and the synthetic default throws MatchException. It signals a build/version mismatch.
saying these in an interview costs you the question
- Adding default 'to be safe' on a sealed switch — it hides the compile error you actually want when the hierarchy grows.
- Thinking guarded arms contribute to exhaustiveness over a sealed type.
- Claiming an exhaustive switch can never fail at runtime — separate compilation can still trigger MatchException.
- Believing exhaustiveness works for an open type like Object without default or a catch-all pattern.