How do record patterns combine with switch and sealed types, including exhaustiveness, guards, and dominance ordering?
answer
- Case labels can be record patterns: branch on type + structure
- Sealed + all subtypes covered => exhaustive, no default needed
- New permitted subtype => non-exhaustive switch fails to compile
- 'when' = guard; guarded case isn't exhaustive alone, put specific first
- case null for null; MatchException when nothing matches at runtime
basics
~20 sYou can use record patterns as switch case labels to branch on the shape of data. If the type being switched is a sealed hierarchy and every permitted subtype is covered, the switch is exhaustive and needs no default. You can refine a case with 'when' (a guard), and you must order cases so a more specific one comes before a more general one.
solid answer
~50 sIn a switch, each case label can be a record pattern, so you branch on both type and structure and bind components per branch. When you switch over a sealed type, the compiler checks exhaustiveness: if every permitted subtype is matched (directly or via patterns), no default arm is needed, and adding a new subtype later turns the incomplete switch into a compile error — a safety win over a runtime default. A 'when' clause adds a boolean guard to a case (e.g. 'case Point(var x, var y) when x == y'); a guarded case only matches if the guard is also true, so an unguarded fallback for the same type should follow it. Cases must be ordered by specificity: a label that dominates (would catch everything a later one would) must not precede it, or you get a compile error. At runtime, if no label matches a non-null value that the compiler couldn't prove exhaustive, a MatchException is thrown; 'case null' handles null explicitly.
code
java · 13 linessealed interface Shape permits Point, Line, Circle {}
record Point(int x, int y) implements Shape {}
record Line(Point start, Point end) implements Shape {}
record Circle(Point center, int radius) implements Shape {}
static String classify(Shape s) {
return switch (s) { // exhaustive: no default needed
case Point(var x, var y) when x == y -> "diagonal point";
case Point(var x, var y) -> "point";
case Line(Point(var x1, var y1), Point(var x2, var y2)) -> "line";
case Circle(Point c, int r) -> "circle r=" + r;
};
}go deeper
Knows record patterns can appear as switch case labels and that switch can branch on the kind of value.
Explains exhaustiveness over a sealed type (no default needed when all subtypes covered) and the basic 'when' guard.
Reasons about dominance ordering, why guarded cases aren't exhaustive alone, null handling (NPE vs case null), and MatchException on unmatched runtime values.
Designs closed sealed hierarchies for compiler-checked totality, weighs no-default exhaustiveness as a maintenance lever, understands separate-compilation skew leading to MatchException, and positions this as ADT/data-oriented programming replacing visitor patterns.
## Record patterns as case labels Java's `switch` evolved from constant-matching into **pattern matching**. A case label may be a **type pattern** (`case Point p`) or a **record pattern** (`case Point(int x, int y)`). With record patterns each branch tests the shape and binds the components it needs: ```java switch (shape) { case Point(var x, var y) -> handlePoint(x, y); case Line(Point(var x1, var y1), Point(var x2, var y2)) -> handleLine(x1, y1, x2, y2); default -> handleOther(); } ``` ## Sealed types and exhaustiveness A **sealed** type restricts which classes may extend/implement it via a `permits` clause, so the compiler knows the *complete* set of subtypes. Example: ```java sealed interface Shape permits Point, Line, Circle {} ``` When a `switch` **expression** (or a switch statement requiring completeness) is over a sealed type, the compiler verifies **exhaustiveness**: are all permitted subtypes covered by the case labels? If yes, **no `default` is required**. This is powerful: add a new permitted subtype (say `Triangle`) and every non-exhaustive switch over `Shape` becomes a **compile-time error**, forcing you to handle it — far safer than a silent runtime `default`. Note a subtlety: a *record pattern* with nested type patterns is not always provably total over the type, because an inner pattern could fail (e.g. a nested reference type test). The compiler considers a case to cover a subtype only when it can prove the match always succeeds for that subtype. ## Guards: the `when` clause A **guarded pattern** adds a boolean condition with `when`: ```java case Point(var x, var y) when x == y -> "on the diagonal"; case Point(var x, var y) -> "off the diagonal"; ``` The case matches only if the pattern matches **and** the guard is true. Because a guard can be false, a guarded case is **not** considered exhaustive on its own — you typically follow it with an unguarded fallback for the same shape. The unguarded case must come **after** the guarded one (specific before general). ## Dominance ordering Case labels are tried top-to-bottom. A label **dominates** a later one if everything the later one matches, the earlier one already matches. The compiler rejects a switch where an earlier label dominates a later, reachable label (the later case would be **unreachable**). Practical rules: more specific record patterns / guarded cases come first; broader type patterns and `default` come last. For example `case Shape s` (or `default`) must not precede `case Point(...)`. ## null and MatchException - By default a pattern switch **throws NullPointerException** on a null selector unless you add `case null` (or `case null, default`). So `case null -> ...` handles null explicitly. - At **runtime**, if the selector is non-null and matches no label in a switch the compiler treated as exhaustive only via the sealed contract, but the actual runtime class falls outside (e.g. separate compilation skew where a new subtype appears), a **MatchException** is thrown rather than silently falling through. This makes incomplete coverage fail loudly. ## Why this matters Record patterns + sealed types + switch give Java **algebraic-data-type style** programming: model a closed set of shapes, then handle each shape's structure in one exhaustive, compiler-checked switch. It replaces visitor boilerplate and instanceof chains with declarative, total, refactor-safe code.
- Why prefer an exhaustive sealed switch with no default over one with a default?With no default, adding a new permitted subtype makes the now-incomplete switch a compile error, so the compiler points you at every place that must handle it. A default silently absorbs the new case, hiding the gap until it misbehaves at runtime.
- Where must the unguarded version of a case go relative to its guarded version?After it. The guarded case ('case Point(...) when cond') is more specific and must precede the unguarded fallback ('case Point(...)'); reversing them makes the guarded case unreachable, a compile error.
saying these in an interview costs you the question
- Claiming a guarded case alone makes a switch exhaustive
- Putting a broad type pattern or default before a specific record pattern (dominance/unreachable error)
- Assuming a pattern switch silently ignores null instead of throwing NPE without case null
- Believing you always need a default even over a sealed hierarchy
- Saying unmatched values fall through silently rather than throwing MatchException