2.3 Pattern Matching for switch and instanceof

Key Takeaways

  • Pattern matching for instanceof binds a typed pattern variable upon a successful type test, with scope determined by compiler flow analysis.
  • Pattern matching for switch (finalized in Java 21 via JEP 441) allows switching over any reference type using type patterns, record patterns, and explicit null labels.
  • Guarded patterns utilize the when clause to attach boolean conditions to pattern cases without nested if statements.
  • Pattern dominance is strictly enforced by the compiler: general supertypes and total patterns must not appear prior to more specific subtype patterns.
  • Exhaustiveness in pattern matching switch is satisfied when all possible hierarchy types (e.g., in sealed classes) or a total pattern/default case are handled.
Last updated: September 2026

Pattern Matching for switch and instanceof

Pattern matching simplifies Java code by combining type testing, casting, and local variable binding into a single declarative operation. Java 16 standardized pattern matching for instanceof (JEP 394), and Java 21 finalized pattern matching for switch (JEP 441) along with Record Patterns (JEP 440). These features form a central pillar of the 1Z0-830 certification exam.


1. Pattern Matching for instanceof

The Problem Pattern Matching Solves

In legacy Java, checking a type and accessing its specific members required a three-step boilerplate pattern: test with instanceof, explicitly cast to the target type, and assign to a new local variable.

// Legacy Java approach:
if (obj instanceof String) {
    String s = (String) obj; // Manual explicit cast
    System.out.println(s.toUpperCase());
}

// Modern Pattern Matching approach:
if (obj instanceof String s) {
    System.out.println(s.toUpperCase()); // 's' is automatically cast and bound!
}

Flow Scoping Rules

Pattern variables do not follow standard lexical block scoping; instead, they use flow scoping. A pattern variable is in scope only on the execution paths where the compiler can guarantee the pattern has matched (definitely assigned).

  1. Logical AND (&&) Scoping: The right operand is only evaluated if the left operand evaluates to true. Therefore, the pattern variable is in scope on the right side of &&:
    if (obj instanceof String s && s.length() > 5) { // LEGAL: s is in scope on right side of &&
        System.out.println(s.toUpperCase());
    }
    
  2. Logical OR (||) Scoping: The right operand is evaluated when the left operand evaluates to false. Therefore, the pattern variable is NOT in scope on the right side of ||:
    // if (obj instanceof String s || s.length() > 5) { } // COMPILE ERROR: cannot find symbol 's'
    
  3. Negation and Early Return Scoping: If an if statement tests !(obj instanceof String s) and terminates with a return, throw, or break, s remains in scope for the rest of the method:
    void printLength(Object obj) {
        if (!(obj instanceof String s)) {
            return; // Exits if NOT a String
        }
        // 's' is in scope here because execution only reaches here if obj WAS a String!
        System.out.println("String length: " + s.length());
    }
    
  4. Shadowing and Scope Clashes: A pattern variable cannot have the same name as an existing local variable in the same scope, but it may shadow a class field.

2. Pattern Matching for switch (Java 21 JEP 441)

In Java 21, the selector expression of a switch construct can be any reference type, and case labels can be type patterns, record patterns, constants, or null.

static String formatValue(Object obj) {
    return switch (obj) {
        case Integer i -> String.format("Integer: %d", i);
        case Long l    -> String.format("Long: %d", l);
        case Double d  -> String.format("Double: %.2f", d);
        case String s  -> String.format("String: '%s'", s);
        case null      -> "Null reference";
        default        -> obj.toString();
    };
}

3. Guarded Patterns with the when Clause

A guarded pattern allows developers to specify a conditional boolean expression on a case label using the when contextual keyword (replacing the preview && syntax):

static void classify(Object obj) {
    switch (obj) {
        case String s when s.length() > 10 -> System.out.println("Long string: " + s);
        case String s when !s.isEmpty()    -> System.out.println("Non-empty string: " + s);
        case String s                      -> System.out.println("Empty string");
        case Integer i when i > 0          -> System.out.println("Positive int: " + i);
        case Integer i                     -> System.out.println("Zero or negative int: " + i);
        default                            -> System.out.println("Other object");
    }
}

Guard Expression Rules:

  • The expression following when must evaluate to a boolean or Boolean.
  • The pattern variable declared in the case label is in scope within the when expression.
  • If the type matches but the when expression evaluates to false, execution skips this case and tests subsequent cases.

4. Pattern Dominance and Ordering Rules

The compiler checks case patterns top-to-bottom and enforces strict dominance rules. If a preceding case pattern matches a superset of all values that a subsequent pattern can match, the preceding pattern dominates the subsequent one, causing a compile-time error for unreachable code.

Dominance Ordering Hierarchy:

  1. Specific Guarded Pattern must precede General Unguarded Pattern:
    // INVALID: Compilation Error!
    switch (obj) {
        case String s -> System.out.println("Any string");
        case String s when s.length() > 5 -> System.out.println("Long string"); // ERROR: Dominated!
    }
    
    // CORRECT ORDER:
    switch (obj) {
        case String s when s.length() > 5 -> System.out.println("Long string");
        case String s -> System.out.println("Any string");
    }
    
  2. Subtype Pattern must precede Supertype Pattern:
    // INVALID: Compilation Error!
    switch (obj) {
        case CharSequence cs -> System.out.println("CharSequence");
        case String s        -> System.out.println("String"); // ERROR: Dominated by CharSequence!
    }
    
    // CORRECT ORDER:
    switch (obj) {
        case String s        -> System.out.println("String");
        case CharSequence cs -> System.out.println("CharSequence");
    }
    
  3. Total Patterns and Default: A pattern case Object o is a total pattern for any Object selector. A total pattern matches all non-null objects and dominates all subsequent type patterns, meaning it must appear last.

5. Null Handling in Pattern Matching Switch

Historically, passing null to a switch statement immediately triggered a NullPointerException. In Java 21, nullability is explicitly integrated into pattern matching:

  1. Explicit case null: Handles null values cleanly without throwing an exception.
    switch (obj) {
        case null -> System.out.println("Object is null");
        case String s -> System.out.println("String: " + s);
        default -> System.out.println("Other");
    }
    
  2. Combined case null, default: Allows handling null and unmatched objects in a single fallback branch:
    switch (obj) {
        case String s -> System.out.println("String: " + s);
        case null, default -> System.out.println("Null or unsupported type");
    }
    
  3. Implicit Null Failure: If the switch selector evaluates to null and neither case null nor case null, default is present, the switch throws a NullPointerException before evaluating any patterns (even if an unguarded default is present!).

6. Record Patterns and Deconstruction (JEP 440)

Java 21 allows deconstructing record components directly inside pattern matching:

record Point(int x, int y) {}
record Line(Point start, Point end) {}

static void printPoint(Object obj) {
    if (obj instanceof Point(int x, int y)) {
        System.out.println("Point at: " + x + ", " + y);
    }
}

static void printLine(Object obj) {
    if (obj instanceof Line(Point(int x1, int y1), Point(var x2, var y2))) {
        System.out.printf("Line from (%d,%d) to (%d,%d)%n", x1, y1, x2, y2);
    }
}

7. Exhaustiveness and Sealed Hierarchies

A pattern matching switch expression must be exhaustive. When switching over a sealed class or interface, if all permitted direct subtypes are covered by case patterns, the switch expression is exhaustive without requiring a default label:

sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double w, double h) implements Shape {}
record Triangle(double b, double h) implements Shape {}

double getArea(Shape shape) {
    return switch (shape) {
        case Circle c    -> Math.PI * c.radius() * c.radius();
        case Rectangle r -> r.w() * r.h();
        case Triangle t  -> 0.5 * t.b() * t.h();
        // No default required! All permitted direct subtypes of Shape are handled.
    };
}

If a new subtype is added to the Shape interface later, the compiler immediately flags the switch expression with a compile-time error, preventing missing branch bugs across codebases.

Loading diagram...
Pattern Matching Switch Evaluation and Dominance Flow
Test Your Knowledge

What is the result of attempting to compile and run the following method?

public static void check(Object obj) {
    if (!(obj instanceof String s)) {
        System.out.print("Not String ");
        return;
    }
    System.out.print(s.toUpperCase());
}

A
B
C
D
Test Your Knowledge

Given the following switch expression, why does the code fail to compile?

public static String evaluate(CharSequence cs) {
    return switch (cs) {
        case CharSequence c -> "Any CharSequence";
        case String s       -> "String: " + s;
    };
}

A
B
C
D
Test Your Knowledge

What is the result of executing the method call process("JAVA")?

public static void process(Object obj) {
    switch (obj) {
        case String s when s.length() < 3  -> System.out.print("Short ");
        case String s when s.length() >= 4 -> System.out.print("Long ");
        case String s                      -> System.out.print("Medium ");
        case null                          -> System.out.print("Null ");
        default                            -> System.out.print("Other ");
    }
}

A
B
C
D
Test Your Knowledge

Given the following sealed hierarchy declarations:

sealed interface Shape permits Circle, Square {}
final class Circle implements Shape {}
final class Square implements Shape {}
Which of the following switch expressions fails to compile?

A
B
C
D