Switch Pattern Matching (Java 21) — Type Patterns, Deconstruction, Guards
Fast recall
Quick revision
Core answer
switch from 'match a constant' to 'match a type and optionally bind a variable,' combined with record deconstruction patterns (JEP 440) and guarded patterns via the when keyword. The resulting construct looks like Scala or Kotlin's when: switch (obj) { case Integer i -> ...; case String s -> ...; case Point(int x, int y) -> ...; case null -> ...; default -> ...; }. Key features: (1) Type patterns — case Integer i -> binds i to the matched value with the correct type. (2) Deconstruction patterns — case Point(int x, int y) -> destructures records directly in the case label. (3) Guards with `when` — case Integer i when i > 100 -> adds a predicate to narrow matching. (4) Null handling — case null -> becomes a valid label (the switch no longer throws NPE by default). (5) Exhaustiveness checking — with sealed classes, the compiler verifies all subtypes are handled and rejects missing cases (no default needed). (6) Nested patterns — case Line(Point(int x1, int y1), Point(int x2, int y2)) -> destructures arbitrarily deep. This brings Java closer to algebraic data types: sealed interfaces + records + pattern switch = the Java equivalent of Haskell's case ... of or Scala's match. It's a major shift from imperative type-checking chains to declarative structural matching.Whether the candidate knows the full pattern switch surface: type patterns, deconstruction, `when` guards, null handling, and exhaustiveness with sealed classes
Forgetting that switch pattern matching throws NPE unless there's an explicit `case null`. Not knowing that deconstruction patterns require records. Thinking exhaustiveness is automatic for non-sealed types
Demonstrate sealed + pattern switch for ADT-style code, explain the ordering rules (compiler rejects unreachable cases), mention pattern matching in instanceof as the predecessor, and discuss the Dominance analysis
Interview-ready guide
Interview answer
Switch pattern matching (Java 21, JEP 441) upgrades switch from 'match a constant' to 'match a type and optionally bind a variable,' combined with record deconstruction patterns (JEP 440) and guarded patterns via when. It brings Java close to Scala/Kotlin's match/when and gives us real algebraic data types.
Core features
1. Type patterns
case Integer i -> ...—iis bound as an Integercase String s -> ...— s is bound as a String- Replaces long
instanceofchains
2. Deconstruction patterns (for records)
case Point(int x, int y) -> ...— destructures the record directly- Nested:
case Line(Point(int x1, int y1), Point(int x2, int y2)) -> ... - Arbitrary depth
3. Guards with `when`
case Integer i when i > 100 -> "large"— adds a predicatecase Point(int x, int y) when x == y -> "diagonal"
4. Null handling
case null -> ...is now a valid label- Without an explicit null case, switch still throws NPE (backward compat)
- Combined:
case null, default -> ...
5. Exhaustiveness checking
- With sealed classes, compiler verifies all subtypes are covered
- No default needed for sealed hierarchies
- Adding a new subtype to the sealed parent = compile error here, forcing update
6. Dominance checking
case CharSequence cs -> ...followed bycase String s -> ...= compile error (unreachable)- Reorder: more specific first
- Two cases with same pattern but different guards = allowed in order
Sealed + records + pattern switch = ADTs
sealed interface Result<T> permits Success, Failure {}
record Success<T>(T value) implements Result<T> {}
record Failure<T>(String error) implements Result<T> {}
<T, R> Result<R> map(Result<T> r, Function<T, R> fn) {
return switch (r) {
case Success<T>(T v) -> new Success<>(fn.apply(v));
case Failure<T>(String e) -> new Failure<>(e);
};
}Same as Scala case Success(v) => ...; slightly more verbose but semantically identical.
Killer use cases
Visitor pattern replacement — 200 lines of visitor classes become 10 lines of switch:
sealed interface Expr permits Lit, Var, Plus, Times {}
int eval(Expr e) {
return switch (e) {
case Lit(int v) -> v;
case Var(String n) -> lookup(n);
case Plus(Expr l, Expr r) -> eval(l) + eval(r);
case Times(Expr l, Expr r) -> eval(l) * eval(r);
};
}Validation results / Either:
sealed interface ResultwithValid(User)/Invalid(List) - Switch handles both branches exhaustively
State machines:
- Sealed interface for states; deconstruct state data in cases
Builds on Java 16's `instanceof` pattern matching
- Same pattern variable binding
- Same flow scoping idea but applied to switch cases
`yield` for multi-statement case blocks
case String s -> { System.out.println(s); yield s.toUpperCase(); }
Performance
invokedynamicviaSwitchBootstraps- Competitive with manual instanceof chains
- No runtime cost for patterns
Pitfalls
1. Null handling: still NPE unless explicit case null
2. Dominance: reorder specific cases before general ones
3. Deconstruction = records only (as of Java 21)
- JEP 468+ working on deconstruction for regular classes
4. Exhaustiveness only for sealed types
switch(obj)on Object always needs default- Sealed types get compile-time exhaustiveness
5. Guards aren't 'total'
- A guard can fail; needs unguarded fallback for each type
When to use
- Sealed hierarchies (replaces visitor)
- Heterogeneous data dispatch (replaces instanceof chains)
- Interpreters, protocol handlers, message processors
- Compile-time exhaustiveness matters
When NOT to use
- Behavior naturally belongs on the type — use polymorphism
- Logic reused across call sites — put it in the class
- Dispatching on constants only — classic switch is fine
Recommendation: Use switch pattern matching as the default whenever you're dispatching on a sealed hierarchy — it replaces the visitor pattern with 10 lines of declarative code and gives you compile-time exhaustiveness so adding a new subtype forces you to update every switch. Combine with records and deconstruction patterns for parser/AST/DTO handling — case Plus(Expr l, Expr r) -> eval(l) + eval(r) is worth writing interpreters for just to use this feature. Reach for `when` guards when you need value-based predicates alongside types, but remember guards aren't 'total' — always have an unguarded fallback per type. Always include `case null` explicitly unless you want NPE. For heterogeneous data without a sealed hierarchy, prefer pattern switch over long instanceof chains — same bytecode, far more readable. Use polymorphism when behavior naturally belongs on the type; pattern switch when dispatching from outside. Between this, sealed classes, records, and virtual threads, Java 21 is the most functional-friendly Java release ever — lean into the style.
Switch Pattern Matching (Java 21) — Type Patterns, Deconstruction, Guards
Direct Answer
Switch pattern matching (Java 21, JEP 441) upgrades switch from 'match a constant' to 'match a type and optionally bind a variable,' combined with record deconstruction patterns (JEP 440) and guarded patterns via the when keyword. The resulting construct looks like Scala or Kotlin's when: switch (obj) { case Integer i -> ...; case String s -> ...; case Point(int x, int y) -> ...; case null -> ...; default -> ...; }. Key features: (1) Type patterns — case Integer i -> binds i to the matched value with the correct type. (2) Deconstruction patterns — case Point(int x, int y) -> destructures records directly in the case label. (3) Guards with when — case Integer i when i > 100 -> adds a predicate to narrow matching. (4) Null handling — case null -> becomes a valid label (the switch no longer throws NPE by default). (5) Exhaustiveness checking — with sealed classes, the compiler verifies all subtypes are handled and rejects missing cases (no default needed). (6) Nested patterns — case Line(Point(int x1, int y1), Point(int x2, int y2)) -> destructures arbitrarily deep. This brings Java closer to algebraic data types: sealed interfaces + records + pattern switch = the Java equivalent of Haskell's case ... of or Scala's match. It's a major shift from imperative type-checking chains to declarative structural matching.
Common Mistakes & Pitfalls
Forgetting that switch pattern matching throws NPE unless there's an explicit case null. Not knowing that deconstruction patterns require records. Thinking exhaustiveness is automatic for non-sealed types
Check your understanding
Answer these without looking back at the article.
- How does pattern switch interact with sealed classes for exhaustiveness?
- What's the difference between a type pattern and a deconstruction pattern?
- Why does the compiler reject `case CharSequence cs` followed by `case String s`?
- What happens if you switch on null without a `case null` clause?
- When would you use `when` guards?
Related interview prep categories
1Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.