Pattern Matching for switch
Pattern Matching for switch
Java 21 (finalized after preview in 17 and 18–20) brought pattern matching for switch — one of the most expressive additions to the language in years. Combined with sealed hierarchies, it lets you write concise, exhaustive, and compiler-verified code that was previously only possible through verbose chains of instanceof checks and casts.
The problem before pattern matching
Suppose you have a sealed Shape hierarchy from the previous lesson:
Before Java 21, computing the area required a cascade of instanceof checks:
This works, but the compiler cannot tell you if you missed a branch. The else throw is a runtime fallback you must remember to write. It also does not scale well as the hierarchy grows.
Type patterns in switch
Pattern matching for switch replaces that cascade with a single expression:
Each case now holds a type pattern: the type to match followed by a binding variable. If the value matches, the binding variable is bound and in scope for that arm. Notice there is no default arm — the compiler knows the switch is exhaustive because Shape is sealed and all three permits are covered.
Triangle case the compiler emits an error: "the switch expression does not cover all possible input values". This is the key benefit over the old instanceof chain — you cannot accidentally forget a subtype.
Guarded patterns (when clauses)
You can add a boolean guard to a case with the when keyword:
Guarded cases are matched top-to-bottom. A large circle matches the first arm; a small circle falls through to the second. Order matters when guards overlap.
case Circle c before the guarded one, the guarded case is unreachable and the compiler will warn you.
Null handling in switch
Traditionally, passing null to a switch throws NullPointerException. With pattern matching, you can handle it explicitly:
Without case null, a null argument would still throw NPE. Adding it makes the intent explicit and avoids surprises.
Combining with enums
Pattern matching also works on enums, and the compiler enforces exhaustiveness there too:
If a new constant is added to Day later, every switch over it that does not have a default will fail to compile — a very useful compiler-enforced safety net.
Deconstruction patterns (preview preview)
Java 21 also previews record deconstruction patterns in switch, letting you match and destructure in one step:
--enable-preview. Use the binding-variable form (case Circle c -> c.radius()) in production code for now.
switch statement vs. switch expression
Everything above uses switch expressions (with -> arms and a value). Pattern matching also works in switch statements (with : arms and break), but the expression form is strongly preferred: it is exhaustiveness-checked and returns a value directly, eliminating the need for a mutable local variable.
Summary
Pattern matching for switch unifies type testing, casting, and binding into a single, exhaustive construct. When paired with sealed classes and records, the compiler guarantees that every subtype is handled, and guards let you express fine-grained conditions without nested if chains. This trio — sealed types, records, and pattern matching — forms the modern Java approach to type-safe, data-oriented programming.