ByteScrollGet the app
☰ Topics
pattern-matching-switch1 / 200›
JAVA / MODERN-JAVA3 minute read

Pattern matching for switch (Java 21)

Medium

Since 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

  1. Type patterns in labels. case Circle c checks instanceof Circle and binds c in one step. No cast, no separate variable.
  2. Guards. case Circle c when c.r() > 10 only matches if the boolean after when is true.
  3. Record patterns. case Rect(double w, double h) matches a Rect record and pulls out its components. Patterns nest: case Line(Point(var x1, var y1), Point p2).
  4. 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.
  5. null. A pattern switch throws NullPointerException on a null selector unless you write case null (optionally case null, default).
  6. Exhaustiveness. A switch expression, or any switch that uses patterns, must cover every possible value. For a sealed interface, listing each permitted subtype is enough.
yesyesnononoyesnoyesswitch (shape)selector isnull?case nullpresent?Run case nullThrowNullPointerExceptionTry next caselabelType / recordpatternmatches?when guardtrue?Bind variablesand run body
yesyesnononoyesnoyesswitch (shape)selector isnull?case nullpresent?Run case nullThrowNullPointerExceptionTry next caselabelType / recordpatternmatches?when guardtrue?Bind variablesand run body

Example

Example.javaJava
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 Shape record 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 throws MatchException at runtime.
  • Guarded cases don't count toward exhaustiveness: case Circle c when ... alone doesn't cover Circle.
  • case null can be combined only with default (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 default to a sealed switch "just in case". It hides the compile error you want when a new subtype appears.
  • Assuming pattern switches handle null like instanceof does. They throw unless case null is 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