How do null selectors and adoption tradeoffs affect choosing arrow switch expressions in a codebase?
answer
- null selector => NPE in classic switch; default does NOT catch it
- Pre-21: guard the selector with x == null
- Java 21: 'case null' (or 'case null, default')
- Exhaustiveness shines over enum + sealed (closed domains)
- Open domains (int/String): keep default + handle null
basics
~20 sA traditional switch throws NullPointerException if the selector is null. Classic arrow switch expressions over enums/strings do the same unless you guard for null first. Adopting them widely improves safety but needs a recent Java version.
solid answer
~50 sA pre-pattern switch (over enum, String, or int) throws NullPointerException when the selector is null — neither arrow nor colon form changes that, and a 'default' branch does NOT catch null in those classic switches. So before Java 21 you must null-check the selector yourself or risk an NPE. From Java 21, pattern-matching switches allow an explicit 'case null' (optionally 'case null, default'), letting the switch handle null as a first-class branch. At the architecture level, adopting arrow switch expressions broadly buys you: no fall-through bugs, compiler-enforced exhaustiveness over enums and sealed hierarchies (so evolving a closed type triggers compile errors), and concise input→value mappings. The tradeoffs are the minimum Java baseline (14 for switch expressions, 21 for pattern/null cases), team familiarity, and deciding a convention (e.g. omit default on enum switches). For genuinely open domains like int/String you still need default and explicit null handling.
go deeper
Knows that a null selector can throw NullPointerException and that you should guard against null.
Explains that default does not catch null in classic switches and that Java 21 adds 'case null'.
Designs switch usage around closed vs open domains, handles null explicitly, and applies the no-catch-all-default convention for enum/sealed coverage.
Sets codebase-wide policy weighing Java baseline (14 vs 21), migration scope, exhaustiveness-as-compile-safety, and null semantics; frames switch expressions within the pattern-matching and sealed-type strategy.
## The null-selector hazard When you write `switch (x) { ... }` and `x` is an **enum**, **String**, or boxed integer, the JVM must read `x` to find the matching label. If `x` is `null`, a classic switch throws a **NullPointerException** *before* any branch — including `default` — runs. This surprises people: they assume `default` is a safety net for "anything," but in the pre-pattern switch, `default` only handles non-null unmatched values. Neither the arrow nor the colon form changes this. ```java Day d = null; int n = switch (d) { // throws NullPointerException — default does NOT catch null case MONDAY -> 1; default -> 0; }; ``` So before Java 21 the correct pattern is an explicit guard: ```java int n = (d == null) ? 0 : switch (d) { case MONDAY -> 1; default -> 0; }; ``` ## Java 21: null as a real case With **pattern matching for switch** (standardized in Java 21), a switch can declare `case null` explicitly, so null becomes a normal branch instead of an exception: ```java int n = switch (d) { case null -> 0; // null handled here case MONDAY -> 1; default -> 0; }; // or combine: case null, default -> 0; ``` If you do *not* write `case null` in such a switch, the null-throws-NPE behavior is preserved for backward compatibility. ## Architectural tradeoffs of adopting arrow switch expressions A principal-level decision is *whether and how widely* to adopt switch expressions. The upsides: - **No fall-through** — eliminates a whole bug class. - **Exhaustiveness over closed domains** — enum and **sealed** hierarchies (types whose subclasses are fixed via `permits`) get compiler-checked coverage. Omitting `default` means adding a new constant/subtype produces compile errors at every switch that must adapt — "make illegal states unrepresentable, surfaced at compile time." - **Conciseness and immutability** — input→value mappings become single expressions, removing mutable accumulator variables. The costs / constraints: - **Java baseline.** Switch expressions need **Java 14+**; `case null` and pattern labels need **Java 21+**. Libraries targeting older runtimes can't use them. - **Convention setting.** Teams should agree: arrow form by default; omit `default` on enum/sealed switches to keep coverage checks; reserve `default` (and explicit null handling) for open domains (`int`, `String`, arbitrary objects). - **Migration scope.** Rewriting legacy colon/break switches is mechanical but large; do it where it reduces real risk (value-mapping switches), not indiscriminately. ## Summary mental model Think of switch expressions as the *typed mapping* tool: best over **closed** domains (enum, sealed) where exhaustiveness and no-fall-through give compile-time guarantees; for **open** domains you accept `default` and must handle null yourself (pre-21) or via `case null` (21+). The architecture payoff is converting "forgot to handle a state" runtime bugs into compile-time errors — provided the team adopts the no-catch-all-default convention and the runtime baseline allows it.
- Does adding a 'default' branch protect a classic switch from a null selector?No. In a pre-pattern switch over enum/String, a null selector throws NullPointerException before any branch — default included. You must null-check the selector, or use 'case null' in a Java 21 pattern switch.
- What single convention most strengthens exhaustiveness benefits?Omit the catch-all 'default' on enum and sealed-type switches. Then adding a new constant or permitted subtype forces a compile error at every switch that must change, instead of silently routing to default.
saying these in an interview costs you the question
- Assuming 'default' catches a null selector (it doesn't in classic switches)
- Thinking switch expressions silently return null for a null selector
- Claiming 'case null' works in any Java version (needs Java 21 pattern switch)
- Adopting switch expressions while ignoring the required Java baseline
- Adding default everywhere, defeating exhaustiveness checks on closed types