Pattern matching for switch (Java 21)
MediumSince Java 21, a case can test a type and bind it: case Circle c ->. Add a guard with when, take records apart with record patterns, and handle null with case null. Over a sealed type the compiler checks every subtype is covered, so no default is needed.
How it works
- Type patterns in labels.
case Circle cchecksinstanceof Circleand bindscin one step. No cast, no separate variable. - Guards.
case Circle c when c.r() > 10only matches if the boolean afterwhenis true. - Record patterns.
case Rect(double w, double h)matches aRectrecord and pulls out its components. Patterns nest:case Line(Point(var x1, var y1), Point p2). - Top to bottom. Cases are tried in source order and the first match wins. A case that can never be reached because an earlier one swallows it (a dominated label) is a compile error.
- null. A pattern switch throws
NullPointerExceptionon anullselector unless you writecase null(optionallycase null, default). - Exhaustiveness. A switch expression, or any switch that uses patterns, must cover every possible value. For a
sealedinterface, listing each permitted subtype is enough.
Example
sealed interface Shape permits Circle, Rect, Square {}
record Circle(double r) implements Shape {}
record Rect(double w, double h) implements Shape {}
record Square(double side) implements Shape {}
static String describe(Shape s) {
return switch (s) {
case null -> "nothing";
case Circle c when c.r() == 0 -> "a dot";
case Circle c -> "circle, area " + Math.PI * c.r() * c.r();
case Rect(double w, double h) when w == h -> "square-ish rect " + w;
case Rect(double w, double h) -> "rect " + w + "x" + h;
case Square sq -> "square " + sq.side();
// no default: the compiler knows these are all the Shapes
};
}Swap the two Circle cases and it no longer compiles: the unguarded case Circle c dominates the guarded one.
Edge cases
- Suppose someone adds a fourth permitted
Shaperecord and ships it without recompiling the class that holds this switch. A value of the new record fits none of the cases, so the exhaustive switch throwsMatchExceptionat runtime. - Guarded cases don't count toward exhaustiveness:
case Circle c when ...alone doesn't coverCircle. case nullcan be combined only withdefault(case null, default ->), not with a type pattern.- Unnamed patterns (
case Circle _) were a preview in 21 and became standard in Java 22. Patterns on primitive types (case int i) arrived later: first previewed in Java 23 (JEP 455), previewed again in 24 and 25, and not yet standard.
Common mistakes
- Adding
defaultto a sealed switch "just in case". It hides the compile error you want when a new subtype appears. - Assuming pattern switches handle
nulllikeinstanceofdoes. They throw unlesscase nullis present. - Putting a broad type (
case Shape s) before narrower ones and being surprised by the dominance error.
Likely follow-up
"How is this different from the old instanceof chain?" It's one expression that yields a value, the compiler checks ordering and coverage, and with sealed types adding a subtype turns every incomplete switch into a compile error instead of a silent fall-through to else.
Get every deep dive in the app
Coming soon to the App StoreComing soon to Google Play