Comprehensive Guide to Pattern Matching for switch in Java

(including Guarded Patterns)

Target Java versions:
- Preview: Java 17 (JEP 406), Java 18 (JEP 420), Java 19 (JEP 427), Java 20 (JEP 433)
- Final / Standard: Java 21 (JEP 441 – Pattern Matching for switch)

This guide covers all core aspects of pattern matching for switch in modern Java,
including type patterns, record patterns, null handling, and guarded patterns using when.


1. What Is Pattern Matching for switch?

Traditionally, switch in Java:

  • Worked only with primitive types, String, and enums.
  • Could not use instanceof-style type tests.
  • Could not destructure structured objects.
  • Was prone to fall-through errors.

Pattern Matching for switch (Java 21+) modernizes it by allowing:

  1. Patterns in case labels:
  2. Type patterns: case String s -> ...
  3. Record patterns: case Point(int x, int y) -> ...
  4. Constant patterns: case 1 -> ..., case "OK" -> ...
  5. null pattern: case null -> ...
  6. Guarded patterns using when:
  7. case String s when s.length() > 5 -> ...
  8. Exhaustiveness checking with sealed types.
  9. Integration with switch expressions (switch (...) { ... }) and switch statements.

The result is a more expressive and safer way to branch on values and structures.


2. Basic Pattern Matching switch Syntax

2.1. Type Pattern Example

String describe(Object obj) {
    return switch (obj) {
        case String s  -> "String of length " + s.length();
        case Integer i -> "Integer value " + i;
        case null      -> "null";
        default        -> "Something else";
    };
}
  • case String s -> is a type pattern:
  • Tests if obj instanceof String
  • Binds s to the casted value ((String) obj)

2.2. Switch Expression vs Switch Statement

Switch expression (returns a value):

String result = switch (obj) {
    case String s  -> "String: " + s;
    case Integer i -> "Int: " + i;
    default        -> "Other";
};

Switch statement (side effects only):

switch (obj) {
    case String s  -> System.out.println("String: " + s);
    case Integer i -> System.out.println("Int: " + i);
    default        -> System.out.println("Other");
}

Both support pattern matching and guarded patterns.


3. Kinds of Patterns in switch

3.1. Constant Patterns

Match fixed constant values:

switch (status) {
    case 200 -> System.out.println("OK");
    case 404 -> System.out.println("Not Found");
    case 500 -> System.out.println("Server Error");
    default  -> System.out.println("Other");
}

3.2. Type Patterns

Match based on type and bind a variable:

switch (obj) {
    case String s  -> System.out.println("String length: " + s.length());
    case Number n  -> System.out.println("Numeric value: " + n);
    default        -> System.out.println("Other type");
}

3.3. Record Patterns (Java 21+)

Given:

record Point(int x, int y) {}

You can destructure:

switch (obj) {
    case Point(int x, int y) -> System.out.println("Point: " + x + ", " + y);
    default                  -> System.out.println("Not a Point");
}

Record patterns can be nested (see §7).


3.4. null Pattern

Java 21 pattern matching makes switch null-aware:

switch (obj) {
    case null      -> System.out.println("It was null");
    case String s  -> System.out.println("String: " + s);
    default        -> System.out.println("Other");
}

If you do not handle null:

  • switch with a null selector will throw NullPointerException.

4. Guarded Patterns (when Keyword)

4.1. Syntax

Guarded patterns refine a match with an extra boolean condition:

case <pattern> when <boolean-expression> -> ...

Example:

String describe(Object obj) {
    return switch (obj) {
        case String s when s.length() > 5 -> "Long string";
        case String s                     -> "Short string";
        default                           -> "Not a string";
    };
}
  • First case: only matches Strings longer than 5 characters
  • Second case: matches all other Strings

Important: when is the correct guard keyword in switch.
case String s && s.length() > 5 is not valid syntax.


4.2. Guard Semantics

case P when C -> ...

Steps:

  1. Try to match pattern P
  2. If match → bind pattern variables (e.g., s)
  3. Evaluate C (the guard)
  4. If C is true → case matches
  5. If C is false → fallthrough; consider next case

Example:

switch (obj) {
    case Integer i when i > 0  -> "Positive";
    case Integer i when i == 0 -> "Zero";
    case Integer i             -> "Negative";
    default                    -> "Not an integer";
}

5. Pattern Matching vs instanceof in if

Patterns also work with instanceof:

if (obj instanceof String s && s.length() > 5) {
    System.out.println("Long string: " + s);
}

Key difference:

  • if uses: obj instanceof String s && condition
  • switch uses: case String s when condition ->
Context Syntax
if if (x instanceof T t && condition)
switch case T t when condition -> ...

6. Exhaustiveness and Sealed Types

Pattern matching works incredibly well with sealed classes/interfaces.

6.1. Sealed Shape Hierarchy Example

sealed interface Shape permits Circle, Rectangle, Square {}

record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
record Square(double side) implements Shape {}

6.2. Exhaustive Switch

double area(Shape s) {
    return switch (s) {
        case Circle c    -> Math.PI * c.radius() * c.radius();
        case Rectangle r -> r.width() * r.height();
        case Square sq   -> sq.side() * sq.side();
    }; // no default needed
}
  • Because Shape is sealed and all permitted subtypes are covered, switch is exhaustive.
  • If a new subtype is added (e.g., Triangle), this switch will fail to compile until updated.

7. Record Patterns & Nested Patterns

Record patterns allow deconstruction and nesting.

7.1. Simple Record Pattern

record Point(int x, int y) {}
record Line(Point from, Point to) {}

String describe(Object obj) {
    return switch (obj) {
        case Point(int x, int y) -> "Point " + x + "," + y;
        default                  -> "Other";
    };
}

7.2. Nested Record Pattern

String describeLine(Object obj) {
    return switch (obj) {
        case Line(Point(int x1, int y1), Point(int x2, int y2)) ->
            "Line from (" + x1 + "," + y1 + ") to (" + x2 + "," + y2 + ")";
        default ->
            "Not a line";
    };
}

7.3. Nested + Guarded

String describeSpecialLine(Object obj) {
    return switch (obj) {
        case Line(Point(int x1, int y1), Point(int x2, int y2))
                when x1 == x2 && y1 == y2 -> "Degenerate line (point)";
        case Line(Point(int x1, int y1), Point(int x2, int y2)) ->
            "Line from (" + x1 + "," + y1 + ") to (" + x2 + "," + y2 + ")";
        default ->
            "Not a line";
    };
}

8. Dominance Rules (Case Order & Reachability)

The compiler checks that cases are not unreachable due to earlier, more general cases.

8.1. Example of Dominance Error

switch (obj) {
    case Object o         -> "Any object";
    case String s         -> "String"; // ❌ unreachable
}

Object o matches all non-null values, so String s is never reached.

8.2. With Guards

switch (obj) {
    case String s when s.isEmpty() -> "Empty string";
    case String s                  -> "Non-empty string";
}

Valid, because:

  • First case matches only empty strings.
  • Second matches non-empty strings.

But this is invalid:

switch (obj) {
    case String s                  -> "Any string";
    case String s when s.isEmpty() -> "Empty"; // ❌ unreachable
}

The guard does not make it more specific than the unguarded version if the unguarded version appears first.


9. Null Handling in Pattern Matching switch

9.1. Explicit null Case

switch (obj) {
    case null      -> "Null";
    case String s  -> "String: " + s;
    default        -> "Other";
}

9.2. NPE When Null Is Not Handled

If obj might be null and you don’t handle case null, then:

switch (obj) {
    case String s -> "String"; // obj == null → NullPointerException
    default       -> "Other";
}

To be safe, always consider null when you don’t control the origin of the selector.


10. Using var in Patterns

You can use var in record patterns to avoid repeating types:

record Box<T>(T value) {}

switch (obj) {
    case Box(var v) -> "Box of " + v;
    default         -> "Not a box";
}

Or mixed:

record Pair<A, B>(A first, B second) {}

switch (p) {
    case Pair(String s, var second) -> "String + " + second;
    case Pair(var first, var second) -> first + " & " + second;
    default -> "Unknown";
}

var means “infer the type from the component type”.


11. Pattern Variable Scope & Flow

Pattern variables (like s, x, y) are in scope:

  • Within their corresponding case body (-> expression or block)
  • Within the guard expression (when ...)

They are not in scope:

  • Outside the switch
  • In previous or later cases
  • In other branches

Example:

switch (obj) {
    case String s when s.length() > 5 -> System.out.println("Long: " + s);
    case String s                     -> System.out.println("Short: " + s);
    default                           -> System.out.println("Not string");
}

// 's' is NOT visible here

12. Limitations & Edge Cases

12.1. Patterns Must Be Compatible with the Selector Type

Number n = ...;

switch (n) {
    case String s -> ... // ❌ compile error (String not compatible with Number)
}

12.2. Guard Expression Must Be Well-Typed

case String s when s.length() > 0 -> ...    // ✅
case String s when s = "x" -> ...           // ❌ invalid: not boolean

12.3. No && Guard in Case Label

This is invalid:

case String s && s.length() > 5 -> ... // ❌

Use when instead.


13. Best Practices

✔ Prefer switch expressions for value-returning logic

String label = switch (status) {
    case 200 -> "OK";
    case 404 -> "Not Found";
    default  -> "Other";
};

✔ Use sealed types + record patterns for ADTs

sealed interface Expr permits Num, Add, Mul {}

record Num(int value) implements Expr {}
record Add(Expr left, Expr right) implements Expr {}
record Mul(Expr left, Expr right) implements Expr {}

int eval(Expr e) {
    return switch (e) {
        case Num(int v)               -> v;
        case Add(Expr l, Expr r)      -> eval(l) + eval(r);
        case Mul(Expr l, Expr r)      -> eval(l) * eval(r);
    };
}

✔ Use guarded patterns instead of nested ifs

case String s when !s.isBlank() && s.length() > 3 -> ...

instead of:

case String s -> {
    if (!s.isBlank() && s.length() > 3) { ... }
}

✔ Handle null explicitly when possible

case null -> ...

❌ Avoid overly complex nested patterns and guards

Prefer readable patterns and extract complex logic into methods.


14. Typical Interview Questions & Answers

Q1. What is pattern matching for switch?

A Java feature (finalized in Java 21) that allows switch to use patterns (type, record, constants, null) instead of only primitive and enum constants, with exhaustiveness checking and pattern variables.


Q2. How do you write a guarded pattern in switch?

case Pattern p when condition -> ...

Example:

case String s when s.length() > 5 -> ...

Q3. Is case String s && s.length() > 5 -> valid?

No. That is invalid syntax. Guards in switch must use when.


Q4. How does pattern matching for switch interact with sealed types?

It allows exhaustive switches without a default, because the compiler knows all possible subtypes and can enforce that you handle all of them.


Q5. How is switch null handling changed by pattern matching?

switch now supports case null, and will throw NPE only if the selector is null and no null case is provided.


Q6. What are record patterns?

Patterns that destructure records:

case Point(int x, int y) -> ...

You can nest them and use them with when guards.


Q7. How is this different from instanceof pattern matching?

  • instanceof pattern matching is used in if conditions (obj instanceof String s).
  • switch pattern matching applies patterns across multiple cases with exhaustiveness checks and supports more pattern forms (including null, record patterns, etc.).

15. Summary

Pattern matching for switch in Java (Java 21+):

  • Brings powerful pattern-based branching to switch
  • Supports:
  • Type patterns
  • Record patterns
  • Constant patterns
  • Null pattern
  • Guarded patterns with when
  • Works best with:
  • Records
  • Sealed classes
  • Record patterns
  • Enables expressive, concise, and type-safe control flow.

This guide is designed to be your one-stop reference for interviews and real-world development using pattern matching for switch in modern Java.