What are guarded patterns (case ... when ...) in a switch, and how do they affect matching and exhaustiveness?
answer
- when = guard, boolean condition on a pattern
- guard false => fall to next label, not throw
- guarded case does NOT count for exhaustiveness
- unguarded case dominates a later guarded same-type case
- order guards before the unguarded fallback
basics
~20 sA guard is a boolean condition added to a case with the when keyword, like case Integer i when i > 0. The arm matches only if the type pattern matches AND the condition is true; otherwise checking continues with the following cases.
solid answer
~50 sA guarded pattern refines a case with an extra boolean test using when: case Integer i when i > 0 -> .... The arm is selected only if the type pattern matches and the guard evaluates to true; if the guard is false, evaluation continues to subsequent labels rather than entering this arm. Guards let you express conditions that types alone cannot, e.g. value ranges. Crucially, a guarded case does NOT count toward exhaustiveness, because the compiler cannot prove the guard is always true. So if you switch over a sealed type and your only arm for a subtype is guarded, you still need an unguarded fallback (an unguarded pattern for that type, or default) to make the switch exhaustive. Guards introduce dominance subtleties: an unguarded case Integer i placed before case Integer i when ... makes the guarded one unreachable. Keep guards ordered from most specific to least, with an unguarded catch for the type.
go deeper
Knows when adds a boolean condition to a case and that the arm matches only if both the type and the condition hold.
Explains that a false guard continues to the next label and that guards express value conditions types cannot.
Knows guarded cases don't satisfy exhaustiveness, understands dominance interactions (unguarded before guarded is a compile error), and orders arms accordingly.
Reasons about readability and maintainability trade-offs of guards vs record patterns, and about how guard-driven non-exhaustiveness interacts with sealed-API evolution and compile-time guarantees.
## What a guard is A **guarded pattern** attaches a boolean condition to a case using the **`when`** keyword (a *contextual keyword*, not a reserved word): ```java String classify(Object o) { return switch (o) { case Integer i when i < 0 -> "negative"; case Integer i when i == 0 -> "zero"; case Integer i -> "positive"; // unguarded fallback for Integer default -> "not an int"; }; } ``` The arm matches only when **both** are true: (1) the **type pattern** matches (the value is an `Integer`, bound to `i`), and (2) the **guard** expression (`i < 0`) evaluates to `true`. The guard can reference the pattern variable, so you test the *value*, not just the type. ### What happens if the guard is false If the type matches but the guard is `false`, this arm is **skipped** and matching **continues** with the following labels. It does not throw and does not abandon the switch. ## Why guards exist Type patterns answer "what kind of thing is this?" Guards answer "...and does it satisfy this condition?" — ranges (`when i > 100`), string contents (`when s.isBlank()`), record field relationships, etc. They keep refinement inside the switch instead of nested `if`s. ## Guards and exhaustiveness **Exhaustiveness** means the compiler can prove every possible input is handled (required for switch *expressions* and for switches over sealed types without `default`). A **guarded** pattern is **not considered exhaustive for its type**, because the compiler cannot evaluate the runtime condition — it cannot know that `when i > 0` will always hold. Therefore: ```java // DOES NOT COMPILE if Shape is sealed and this is the only Circle arm: case Circle c when c.r() > 0 -> ... // guarded -> doesn't cover all Circles ``` You must add an **unguarded** arm for that type (or a `default`) so every value is covered. Rule of thumb: *a guarded case never closes a hole; only an unguarded pattern or `default` does.* ## Dominance with guards **Dominance** is the rule that a label appearing earlier must not make a later label unreachable. An **unguarded** `case Integer i` **dominates** any later `case Integer i when ...` because the unguarded one already catches every `Integer` — the guarded arm becomes dead code and the compiler errors. So order guards **before** the unguarded fallback for the same type, typically most-specific first. ## Terms recap - **when**: contextual keyword introducing the guard; only valid after a pattern. - **Guard**: any boolean expression; may use the pattern variable. - **Exhaustiveness**: compile-time proof all inputs are handled. - **Dominance**: ordering constraint preventing unreachable arms. ## Gotchas - A guard that throws propagates out of the switch. - Guards can make logic hard to read if overused; sometimes a record pattern or a plain `if` inside the arm is clearer. - `when` is contextual, so existing variables/methods named `when` still compile elsewhere.
- You switch over a sealed interface Shape and write only case Circle c when c.r() > 0 -> ... and case Square s -> .... Why won't it compile?The Circle arm is guarded, so the compiler cannot prove all Circles are covered. The switch is not exhaustive. Add an unguarded case Circle c (or a default) so every Circle is handled.
saying these in an interview costs you the question
- Thinking a single guarded case over a sealed subtype is enough for exhaustiveness — it isn't; you still need an unguarded arm or default.
- Placing an unguarded case Integer i before case Integer i when ... (the guarded arm becomes unreachable, a compile error).
- Believing a false guard throws or exits the switch — it just continues to the next label.
- Calling when a reserved keyword; it is contextual.