Pattern Matching for switch
Pattern matching for switch, finalized in Java 21 (JEP 441, after preview across Java 17–20), extends switch's case labels beyond plain constant matching to also support type patterns — testing a selector's runtime type and binding a correctly-typed pattern variable directly within the case label — along with guarded patterns (when clauses), explicit null handling via case null, and compiler-verified exhaustiveness over sealed type hierarchies, turning switch into a general-purpose, type-dispatching control structure rather than the constant-comparison-only construct it had been since Java 1.0. This entry covers exactly what kind of chained instanceof/else-if logic this replaces and why that logic was error-prone, type patterns versus traditional constant case labels and how they interact in one switch, guarded patterns and case label ordering/dominance rules, and the explicit case null label alongside how unconditional vs conditional patterns affect exhaustiveness and the need for a default.
Motivation — Replacing Chained instanceof/else-if Type Dispatch
// ── Before Java 21 — type dispatch via chained instanceof/else-if ──────
String describeOld(Object obj) {
if (obj instanceof Integer i) {
return "Integer: " + i;
} else if (obj instanceof Long l) {
return "Long: " + l;
} else if (obj instanceof String s) {
return "String of length " + s.length();
} else if (obj instanceof Double d) {
return "Double: " + d;
} else {
return "Unknown: " + obj;
}
}
// No compiler-verified guarantee that every relevant type was actually
// considered — purely a sequence of independent if/else-if boolean checks
// ── Java 21 — pattern matching for switch: a single, scannable dispatch ─
String describe(Object obj) {
return switch (obj) {
case Integer i -> "Integer: " + i;
case Long l -> "Long: " + l;
case String s -> "String of length " + s.length();
case Double d -> "Double: " + d;
default -> "Unknown: " + obj;
};
}
// Reads directly as "dispatch on type" — and (as covered below) gains
// real compiler exhaustiveness checking when the selector is a sealed typeType Patterns, Constant Labels in the Same Switch, and Dominance Rules
// ── Mixing constant labels and type patterns in one switch ─────────────
String classify(Object obj) {
return switch (obj) {
case 0 -> "zero literal"; // constant label — still works as always
case Integer i -> "Integer: " + i; // type pattern — for any other int value
case String s when s.isEmpty() -> "empty string"; // guarded type pattern
case String s -> "String: " + s;
default -> "Other: " + obj;
};
}
// ── Dominance rule — compiler rejects an earlier label that swallows a later one
Object value = "test";
String result = switch (value) {
case Object o -> "Any object: " + o; // matches EVERYTHING — placed first
// case String s -> "String: " + s; // COMPILE ERROR if uncommented:
// "this case label is dominated by a preceding case label" — String is
// unreachable here, since "Object o" above already matches every String too
default -> "unreachable anyway";
};
// ── Correct ordering — most specific pattern first, general pattern last ─
String resultFixed = switch (value) {
case String s -> "String: " + s; // more specific — checked FIRST
case Object o -> "Any object: " + o; // general fallback — checked LAST, legal here
};case null, Guarded Patterns, and Exhaustiveness Implications
// ── case null — handled explicitly within the switch itself ────────────
String describe(Object obj) {
return switch (obj) {
case null -> "Got null"; // explicit opt-in; no separate pre-check needed
case Integer i -> "Integer: " + i;
default -> "Something else: " + obj;
};
}
// Without "case null", a null selector here would still throw NullPointerException,
// exactly as a traditional switch always has — unchanged unless opted into
// ── case null, default — combined label for shared null/default handling
String describeCombined(Object obj) {
return switch (obj) {
case Integer i -> "Integer: " + i;
case null, default -> "Null or unrecognized: " + obj;
// both null AND anything not matched above receive THIS same treatment
};
}
// ── Guarded (conditional) patterns don't by themselves satisfy exhaustiveness
sealed interface Shape permits Circle, Square {}
record Circle(double radius) implements Shape {}
record Square(double side) implements Shape {}
// double areaBad(Shape shape) {
// return switch (shape) {
// case Circle c when c.radius() > 0 -> Math.PI * c.radius() * c.radius();
// case Square s -> s.side() * s.side();
// // COMPILE ERROR — "Circle c when ..." is a CONDITIONAL pattern; the guard
// // might be false at runtime, so Circle isn't provably fully covered —
// // exhaustiveness over the sealed Shape hierarchy is NOT satisfied
// };
// }
// Fix — add an unconditional fallback for the guarded type, or a default:
double areaFixed(Shape shape) {
return switch (shape) {
case Circle c when c.radius() > 0 -> Math.PI * c.radius() * c.radius();
case Circle c -> 0.0; // unconditional fallback for Circle — now covered
case Square s -> s.side() * s.side();
// exhaustive now — Circle is fully covered (guarded + unconditional fallback),
// Square is fully covered (unconditional) — no default required
};
}