Java Pattern Matching for switch

Java Programming Tutorials


Java pattern matching for switch lets a case test the runtime type and structure of a value. When a pattern matches, Java also creates variables that can be used directly by that case.

The feature became permanent in Java 21. It makes data-oriented branching more concise than a long sequence of instanceof checks and casts.

Match Values by Type

A type pattern consists of a type followed by a pattern variable. The variable is initialized only when the selected value matches that type.

Example

static String describe(Object value) {
    return switch (value) {
        // Each successful pattern creates a typed variable
        case Integer number -> "Integer: " + number;
        case Double number  -> "Double: " + number;
        case String text    -> "Text: " + text;
        default             -> "Other: " + value;
    };
}

No explicit cast is needed. Inside the String rule, for example, text already has the type String.

Use a switch Expression or Statement

Pattern labels work in both switch expressions and switch statements. A switch expression returns a value and must be exhaustive.

Switch expression

static int textLength(Object value) {
    // Every possible non-null type has a matching rule
    return switch (value) {
        case CharSequence text -> text.length();
        default                -> 0;
    };
}

A switch statement performs actions instead of producing a value. A statement that uses pattern or null labels must also be exhaustive.

Switch statement

static void printType(Object value) {
    switch (value) {
        // Arrow rules avoid accidental fall-through
        case String text -> System.out.println("String: " + text);
        case Number number -> System.out.println("Number: " + number);
        default -> System.out.println("Another reference type");
    }
}

Handle null Inside switch

A case null label handles a null selector directly. Without it, switching on a null reference still throws NullPointerException. The default label alone does not match null.

Example

static String normalize(Object value) {
    return switch (value) {
        // Handle null without a separate if statement
        case null        -> "missing";
        case String text -> text.trim();
        default          -> value.toString();
    };
}

You can combine the remaining null and unmatched cases when they perform the same action.

Example

static String category(Object value) {
    return switch (value) {
        case Integer number -> "whole number";
        case Double number  -> "decimal number";
        // Match null and every otherwise unmatched value
        case null, default  -> "other";
    };
}

Refine a Pattern with when

A guarded pattern adds a boolean condition with when. The pattern must match and its guard must evaluate to true.

Example

static String classify(Object value) {
    return switch (value) {
        // Test the type and an additional condition
        case String text when text.isBlank() -> "blank text";
        case String text when text.length() < 10 -> "short text";
        case String text -> "long text";
        case null -> "missing";
        default -> "not text";
    };
}

The pattern variable is in scope within the guard and the corresponding rule. Guards belong to pattern labels; a constant case label cannot independently use a when guard.

Order Cases from Specific to General

More than one type pattern may theoretically match the same value. Java uses the first applicable label and rejects a label that is dominated by an earlier label.

Incorrect order

static void invalidOrder(Object value) {
    switch (value) {
        // CharSequence already matches every String
        case CharSequence text -> System.out.println(text.length());

        // Compile-time error: this case is dominated
        case String text -> System.out.println(text.toUpperCase());

        default -> System.out.println("Other");
    }
}

Place constants first, guarded patterns next, and broader unguarded patterns last.

Correct order

static String inspect(Object value) {
    return switch (value) {
        // Special and guarded cases come before the general type
        case "admin" -> "reserved name";
        case String text when text.isBlank() -> "blank";
        case String text -> "text: " + text;
        default -> "other";
    };
}

Tip: Think of pattern case ordering like exception handlers: place narrower cases before cases that accept a wider set of values.

Write an Exhaustive Pattern switch

A pattern switch must cover every possible selector value. A match-all default or unconditional type pattern can provide coverage for an open type such as Object.

Example

static int sizeOf(Object value) {
    return switch (value) {
        case null -> 0;
        case String text -> text.length();
        case int[] values -> values.length;

        // Covers all remaining non-null Object values
        case Object ignored -> 1;
    };
}

An unconditional pattern that covers the selector type acts as a match-all rule. It cannot appear together with a separate default because both would cover the same remaining values.

Use Sealed Types for Complete Coverage

When a selector has a sealed type, the compiler knows its permitted direct subtypes. A switch can cover those alternatives without a default rule.

Example

sealed interface Payment permits CardPayment, CashPayment {
}

record CardPayment(double amount, String lastFour)
        implements Payment {
}

record CashPayment(double amount)
        implements Payment {
}

static double amountOf(Payment payment) {
    // All permitted Payment implementations are covered
    return switch (payment) {
        case CardPayment card -> card.amount();
        case CashPayment cash -> cash.amount();
    };
}

Omitting an unnecessary default helps the compiler report a missing case after a new permitted subtype is added and the switch is recompiled.

Combine switch with Record Patterns

Record patterns, also permanent since Java 21, can deconstruct a record directly in a case label. This combines type testing, casting, and component extraction.

Example

sealed interface Shape permits Circle, Rectangle {
}

record Circle(double radius) implements Shape {
}

record Rectangle(double width, double height) implements Shape {
}

static double area(Shape shape) {
    return switch (shape) {
        // Extract record components directly in each case
        case Circle(double radius) ->
            Math.PI * radius * radius;

        case Rectangle(double width, double height) ->
            width * height;
    };
}

Use Nested Record Patterns

A record component can itself be matched by another record pattern.

Example

record Point(int x, int y) {
}

record Line(Point start, Point end) {
}

static String describeLine(Line line) {
    return switch (line) {
        // Deconstruct Line and both nested Point values
        case Line(Point(int x1, int y1), Point(int x2, int y2))
            when x1 == x2 -> "Vertical line";

        case Line(Point(int x1, int y1), Point(int x2, int y2))
            when y1 == y2 -> "Horizontal line";

        case Line(Point start, Point end) ->
            "Diagonal from " + start + " to " + end;
    };
}

Pattern Variable Scope

A pattern variable exists only where Java knows the pattern matched:

  • Inside the guard belonging to its case.
  • Inside the expression, statement, or block to the right of an arrow rule.
  • Inside the statements belonging to a colon-style case group.
  • Not inside other case rules or after the switch.

Arrow rules are usually clearer because each pattern variable has an obvious, isolated scope and accidental fall-through is impossible.

Important Rules

Rule Meaning
Compatibility Every pattern must be compatible with the selector's declared type.
Dominance An earlier case cannot make a later case unreachable.
Exhaustiveness Every possible value must be covered by a pattern switch.
Null handling Null requires a null label or causes NullPointerException.
Guard evaluation A guard runs only after its pattern has matched.
Variable scope A pattern variable is available only where the match is known to succeed.

Best Practices

  • Order constants and narrow patterns before broader type patterns.
  • Use guards for short conditions directly related to the pattern.
  • Handle null explicitly when null is a valid input.
  • Avoid a default rule for sealed hierarchies when listing every subtype is practical.
  • Use polymorphic methods instead when behavior naturally belongs to each class.
  • Prefer arrow rules to prevent fall-through and clarify variable scope.

Conclusion

Java pattern matching for switch provides concise type dispatch, scoped variables, guards, explicit null handling, and compile-time completeness checks. It is especially effective with sealed hierarchies and record patterns, provided cases are ordered from specific to general and the switch remains exhaustive.



Found This Page Useful? Share It!
Get the Latest Tutorials and Updates
Join us on Telegram