JavaPattern Matching

Pattern Matching

Pattern matching lets you test whether a value has a certain shape or type, and — if it matches — immediately bind part of it to a new variable, all in one step. Java has added pattern matching incrementally: first to instanceof (Java 16), then to switch (Java 21), and it dramatically cuts down the defensive cast-after-check boilerplate that used to be everywhere in Java code.

Pattern matching for instanceof

Before Java 16, checking a type with instanceof and then using the value as that type required a separate, explicit cast right after the check — the compiler already knew the type was safe to cast, but you still had to write the cast yourself.

Before: check, then cast

Java
Object obj = "hello";

if (obj instanceof String) {
    String s = (String) obj; // redundant cast — we already checked!
    System.out.println(s.toUpperCase());
}

Pattern matching for instanceof lets you name the cast variable right in the instanceof expression. If the check succeeds, that variable is automatically populated with the value, already cast — and it's only in scope where the compiler can prove the check passed.

After: instanceof binds the variable directly

Java
Object obj = "hello";

if (obj instanceof String s) {
    System.out.println(s.toUpperCase()); // s is already a String, no cast
}

// The pattern variable's scope can extend past the if when the compiler
// can prove it must have matched — handy for early-return guard clauses
void process(Object obj) {
    if (!(obj instanceof String s)) {
        return;
    }
    System.out.println(s.toUpperCase()); // s is in scope here too
}
Pattern matching for switch

Java 21 extends pattern matching into switch, so a switch expression or statement can match directly on an object’s runtime type — each case acts like a type-checked, auto-cast branch, replacing long if / else if (x instanceof Y) chains.

Before: an if/else if chain of instanceof checks

Java
String describe(Object obj) {
    if (obj instanceof Integer i) {
        return "An integer: " + i;
    } else if (obj instanceof String s) {
        return "A string of length " + s.length();
    } else if (obj == null) {
        return "It's null";
    } else {
        return "Something else: " + obj;
    }
}

After: switch pattern matching (Java 21)

Java
String describe(Object obj) {
    return switch (obj) {
        case Integer i -> "An integer: " + i;
        case String s  -> "A string of length " + s.length();
        case null      -> "It's null";
        default        -> "Something else: " + obj;
    };
}

Notice switch pattern matching can even match null directly as a case, something a classic switch never allowed (it used to throw a NullPointerException on a null subject).

Record patterns (deconstruction)

Pattern matching goes a step further with record patterns: if the type being matched is a record, you can deconstruct it right in the pattern, binding variables to its components directly instead of matching the whole record and then calling accessor methods.

Deconstructing a record in a switch pattern

Java
record Point(int x, int y) {}

String classify(Object obj) {
    return switch (obj) {
        case Point(int x, int y) when x == 0 && y == 0 -> "origin";
        case Point(int x, int y) when x == y            -> "on the diagonal";
        case Point(int x, int y)                        -> "point at (" + x + ", " + y + ")";
        default                                         -> "not a point";
    };
}

The when clause adds a further boolean guard on top of the matched pattern, letting you express conditions that go beyond just type and shape. Record patterns can even nest, deconstructing a record whose component is itself another record. (See the Records page for more on what makes a type a record in the first place.)

Why this matters: exhaustiveness

Pattern matching in switch pairs naturally with sealed classes, because the compiler knows the complete, closed set of subtypes a sealed hierarchy permits. When every possible subtype has a case, the compiler can verify the switch is exhaustive and skip requiring a default branch — turning what used to be a runtime risk (an unhandled subtype silently falling through) into a compile-time guarantee. (See the Sealed Classes page for the hierarchy-restriction side of this story.)

Boilerplate that pattern matching removes
Before pattern matching, safely narrowing a type from `Object` (or a supertype) meant: check with `instanceof`, cast explicitly, and assign to a new variable — three steps, repeated at every call site. Pattern matching for `instanceof` and `switch` folds all three into the check itself, and record patterns extend that folding to an object’s internal components too.
Tip
Favor pattern-matching `switch` over chained `instanceof` checks once you have three or more type branches — it reads as one decision instead of a sequence of early exits, and the compiler will flag a missing case if the set of types is sealed.
  • instanceof pattern matching (Java 16+) binds a cast variable directly in the check: if (obj instanceof String s).

  • Pattern matching for switch (Java 21) matches case labels against runtime types, including case null.

  • Record patterns deconstruct a record's components directly in a pattern, optionally guarded by a when clause.

  • Pattern matching combined with sealed classes lets the compiler verify a switch handles every possible subtype.