Java Pattern Matching for switch (Java 21+): What It Is and Why It Matters

Date:

Category: Core Java


Java’s pattern matching for switch, finalized in Java 21, lets you test a value’s type and deconstruct it directly in a switch. It reduces boilerplate instanceof checks, improves readability, and enables safer, more expressive code. As of Monday, November 10, 2025 (UTC), this feature is stable in mainstream JDKs, so you can use it in production without preview flags.Key benefits:

  • Cleaner code by combining type testing and extraction.
  • Exhaustive switching with sealed hierarchies—often without a default.
  • Guards (the when clause) for refined, readable conditions.
  • Built-in null handling in switch.

SEO keywords to know: Java pattern matching for switch, Java 21 switch patterns, guarded patterns, record patterns, sealed classes, switch expression.

Getting Started: Basic switch Patterns

Pattern matching allows different actions depending on the runtime type. You can also use switch expressions to return values concisely.

text
static String describe(Object obj) {
    return switch (obj) {
        case Integer i -> "Integer: " + i;
        case String s when s.isBlank() -> "Blank String";
        case String s -> "String: " + s;
        case null -> "It’s null";
        default -> "Unknown type: " + obj.getClass().getSimpleName();
    };
}

Why this is better:

  • No manual casts; the variable i or s is already typed.
  • Guards like when s.isBlank() let you branch cleanly.
  • Null-safe: you can explicitly handle null in the switch.

Guarded Patterns: Add Conditions Inline

Guards use when to add an extra boolean condition to a case. This keeps branching logic in one place, reducing nested if statements.

text
static String classifyNumber(Number n) {
    return switch (n) {
        case Integer i when i < 0 -> "Negative int";
        case Integer i when i == 0 -> "Zero int";
        case Integer i -> "Positive int";
        case Long l when l > 1_000_000L -> "Large long";
        case Double d when Double.isNaN(d) -> "NaN double";
        default -> "Some other number";
    };
}

Best practice: Order guarded cases from most specific to least specific to avoid unreachable branches.

Handling null in switch

Unlike older Java, pattern matching for switch lets you match null explicitly. This encourages safer code and avoids accidental NPEs.

text
static String safeToString(Object o) {
    return switch (o) {
        case null -> "<null>";
        case String s -> s;
        default -> o.toString();
    };
}

Tip: If you omit case null, and o is null, the switch will throw a NullPointerExceptionAdd a case null when nulls are possible.

Exhaustive Switches with Sealed Hierarchies

When you use a sealed interface or class, the compiler knows all permitted subtypes. Your switch can become exhaustive without a default, improving safety during future refactors.

text
sealed interface Shape permits Circle, Rectangle, Square {}
record Circle(double r) implements Shape {}
record Rectangle(double w, double h) implements Shape {}
record Square(double s) implements Shape {}

static double area(Shape shape) {
    return switch (shape) {
        case Circle c -> Math.PI * c.r() * c.r();
        case Rectangle r -> r.w() * r.h();
        case Square sq -> sq.s() * sq.s();
    };
}

Why this is great:

  • No default needed; if you add a new subtype, the compiler flags missing cases.
  • Refactor-friendly and type-safe.

Record Patterns Inside switch

Record patterns (final in Java 21) pair perfectly with switch patterns for deconstruction and matching.

text
record Point(int x, int y) {}
record Line(Point p1, Point p2) {}

static String describeLine(Object o) {
    return switch (o) {
        case Line(Point(int x1, int y1), Point(int x2, int y2))
            when x1 == x2 -> "Vertical line";
        case Line(Point p1, Point p2) -> "Line from %s to %s".formatted(p1, p2);
        default -> "Not a line";
    };
}

You can nest record patterns and use guards to express domain logic succinctly.

Dominance and Reachability: Avoid Common Pitfalls

The compiler checks that no case is dominated by a previous, more general case.Example of a compile-time error:

text
static String bad(Object o) {
    return switch (o) {
        case CharSequence cs -> "CharSequence";
        case String s -> "String"; // Error: dominated by CharSequence
        default -> "Other";
    };
}

Fix by reversing order or adding guards:

text
static String good(Object o) {
    return switch (o) {
        case String s -> "String";
        case CharSequence cs -> "CharSequence";
        default -> "Other";
    };
}

Rule of thumb: Put more specific patterns first, then broader ones.

Best Practices and Migration Tips

  • Target Java 21+ to use pattern matching for switch and record patterns without preview flags.
  • Prefer switch expressions when you compute and return a value; they are concise and reduce mutable state.
  • Handle null explicitly if nulls may appear in your domain.
  • Leverage sealed hierarchies to achieve exhaustiveness and catch omissions at compile time.
  • Use guards sparingly but purposefully to keep matching logic readable.
  • Avoid overly broad types before specific ones to prevent dominance issues.

Wrap-Up

Java’s pattern matching for switch in Java 21+ brings concise, expressive, and safer branching to everyday code. With guarded patterns, record deconstruction, and sealed-type exhaustiveness, you can replace verbose instanceof chains with readable, maintainable switches. Adopt it in new code, incrementally refactor older code paths, and enjoy the compile-time checks and clarity this modern Java feature provides.


x