skip to content

How do null selectors and adoption tradeoffs affect choosing arrow switch expressions in a codebase?

level: principalimportance: nice to knowfreq 30%

answer

  1. null selector => NPE in classic switch; default does NOT catch it
  2. Pre-21: guard the selector with x == null
  3. Java 21: 'case null' (or 'case null, default')
  4. Exhaustiveness shines over enum + sealed (closed domains)
  5. Open domains (int/String): keep default + handle null

basics

~20 s

A 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 s

A 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

for a junior

Knows that a null selector can throw NullPointerException and that you should guard against null.

for a middle

Explains that default does not catch null in classic switches and that Java 21 adds 'case null'.

for a senior

Designs switch usage around closed vs open domains, handles null explicitly, and applies the no-catch-all-default convention for enum/sealed coverage.

for a principal

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

context