Pattern Matching for instanceof

Pattern matching for instanceof (Java 16+) fuses a type check and a cast into one expression, eliminating the classic redundant-cast boilerplate.

Pattern matching for instanceof, standardized in Java 16 (previewed in 14 and 15), lets an instanceof check bind the successfully-tested value to a new typed variable — a pattern variable — directly in the condition, removing the separate explicit cast that traditionally followed.

The Problem It Solves

Before pattern matching, checking a type and then using the value as that type required a check, then a redundant cast repeating the same type.

// Traditional style (pre-Java 16)
void describe(Object obj) {
    if (obj instanceof String) {
        String s = (String) obj; // redundant cast - we already know it's a String
        System.out.println("Length: " + s.length());
    }
}

Pattern Variables

With pattern matching, you write the target type and a new variable name directly in the instanceof expression. If the check succeeds, that variable is automatically bound to the value, already cast to the target type — no separate cast statement needed.

s is called the pattern variable — it's introduced by the type pattern String s and is only usable where the compiler can prove obj was actually a String.

void describe(Object obj) {
    if (obj instanceof String s) {
        System.out.println("Length: " + s.length()); // s is already a String here
    }
}

Scope: Flow-Sensitive Typing

A pattern variable's scope is not simply 'the rest of the enclosing block' — it is flow-sensitive: the variable is definitely usable only where the compiler can prove, from the surrounding control flow, that the instanceof check must have been true. This is sometimes called flow scoping or 'definite unassignment' analysis working in reverse.

void demo(Object obj) {
    if (obj instanceof String s) {
        System.out.println(s.length()); // in scope: check was true here
    }
    // System.out.println(s.length()); // compile-time error: s not in scope here
}

Negated Checks Extend Scope

A common, powerful idiom: negate the instanceof check and return/continue/throw/break in the if body — the pattern variable then remains in scope for the rest of the enclosing block, because the compiler can prove that if execution reaches past the if, the pattern must have matched.

void process(Object obj) {
    if (!(obj instanceof String s)) {
        return; // if we get past this line, obj instanceof String was true
    }
    System.out.println(s.toUpperCase()); // s is in scope here

Scope Across && and ||

In a && (logical AND) expression, a pattern variable bound by the left operand is in scope for the right operand, because && only evaluates the right side if the left was true. In || (logical OR), the pattern variable from the left side is NOT in scope on the right, because the right side is evaluated precisely when the left side (and thus the pattern match) was false.

void demo(Object obj) {
    if (obj instanceof String s && s.length() > 3) { // OK: s is in scope for the && right side
        System.out.println(s);
    }

    // if (obj instanceof String s || s.length() > 3) { } // compile-time error: s not definitely assigned here
}

Flow-Sensitivity in Practice

The compiler performs real flow analysis, not just textual block scoping — it tracks every path by which control could reach a given point and only allows the pattern variable where it's guaranteed bound on all of them.

void demo(Object obj) {
    boolean ok = obj instanceof String s && !s.isEmpty();
    if (ok) {
        // s is NOT in scope here - the compiler doesn't track it across the boolean assignment
    }
}

Comparison: Before and After

Before (pre-16)                          After (16+, pattern matching)
----------------                         -----------------------------
if (x instanceof Foo) {                  if (x instanceof Foo f) {
    Foo f = (Foo) x;                          use(f);
    use(f);                              }
}

Interaction With equals() Overrides

A very common real use is simplifying equals() implementations, which traditionally needed an instanceof check followed by a cast to compare fields.

One expression handles the null check (instanceof is false for null), the type check, the cast, and the field comparison.

final class Point {
    private final int x, y;
    Point(int x, int y) { this.x = x; this.y = y; }

    @Override
    public boolean equals(Object o) {
        return o instanceof Point p && x == p.x && y == p.y;
    }
}

Java Version

Pattern matching for instanceof was a preview feature in Java 14 (JEP 305) and 15, and became a standard, permanent language feature in Java 16 (JEP 394). Code targeting Java 15 or earlier needs --enable-preview to use it (and it may still change), while Java 16+ has it available by default with no special flags.

Key Takeaways

Pattern matching for instanceof merges a type check and a cast into if (obj instanceof Type var).

The pattern variable's scope is flow-sensitive, not simple block scope — it's only usable where the match is provably true.

Negating the check with an early return extends the pattern variable's scope past the if-block.

A pattern variable from the left of && is in scope on the right; from the left of || it is not.

It became a standard, non-preview feature in Java 16.

if (obj instanceof String) { String s = (String) obj; // redundant, old style } if (obj instanceof String s) { // s already usable, no separate cast }

Pattern matching folds the cast into the instanceof check itself — writing the manual cast afterward defeats the purpose of the feature.

if (obj instanceof String s || s.isEmpty()) { } // compile error if (obj instanceof String s && s.isEmpty()) { } // or restructure with || if (!(obj instanceof String s) || s.isEmpty()) { } // valid: negation flips the flow-sensitivity

On the right side of ||, the left side's pattern match was false (that's why || evaluated the right side), so s is not guaranteed bound there — the compiler rejects it.

if (obj instanceof String s) { //... } System.out.println(s); // compile error: s not in scope if (!(obj instanceof String s)) { return; } System.out.println(s); // s is in scope here

A plain (non-negated) if-block confines the pattern variable's proven scope to that block; to use it afterward, negate the check and exit early on failure.

What does pattern matching for instanceof eliminate compared to traditional code?

It eliminates the separate, redundant explicit cast that traditionally followed a successful instanceof check — the pattern variable is already bound to the correctly-typed value inside the matching branch.

What does 'flow-sensitive scope' mean for a pattern variable?

The variable is only usable at points in the code where the compiler can prove, through control-flow analysis, that the instanceof check must have succeeded — not simply 'the rest of the textual block' the way ordinary local variable scope works.

Why is a pattern variable from the left operand of && usable on the right operand, but not for ||?

&& only evaluates its right operand when the left operand was true, so the pattern match is guaranteed to have succeeded by then. || only evaluates its right operand when the left was false, meaning the pattern match failed, so the variable can't be guaranteed bound.

How does negating an instanceof pattern check with an early return extend the pattern variable's usable scope?

By writing if (!(obj instanceof Type v)) { return; }, any code after that if-block can only be reached when the negated condition was false — meaning the original instanceof check was true — so the compiler allows v to be used for the rest of the enclosing block.

In which Java version did pattern matching for instanceof become a standard (non-preview) feature?

Java 16 (JEP 394), after being previewed in Java 14 and 15.

How does pattern matching for instanceof commonly simplify equals() implementations?

It combines the null check (instanceof returns false for null), the type check, and the cast into a single boolean expression: return o instanceof MyClass m && field == m.field;, replacing several separate lines.

If obj instanceof String s evaluates to false, is s definitely assigned any value?

No — s is simply not definitely assigned in that branch; the compiler tracks this and refuses to compile any use of s at a point it can't prove the match succeeded.

Rewrite this traditional instanceof-and-cast code using pattern matching for instanceof: if (shape instanceof Circle) { Circle c = (Circle) shape; System.out.println(c.radius()); } if (shape instanceof Circle c) { System.out.println(c.radius()); }

The pattern Circle c performs the type check and cast together; c is already a Circle inside the if-block, with no separate cast line needed.

Does this compile, and what does it print?

It compiles and prints 4. The negated check with an early return proves that any code after the if-block only runs when o was a String, so s is in scope and usable there.

Write a method areEqualStrings(Object a, Object b) that returns true only if both are Strings and equal, using pattern matching for instanceof and &&.

static boolean areEqualStrings(Object a, Object b) { return a instanceof String sa && b instanceof String sb && sa.equals(sb); }

Both instanceof checks bind sa and sb via &&-chained pattern matches; sa.equals(sb) is only evaluated once both prior checks succeeded, since && short-circuits left to right.

Does this code compile? Explain why or why not.

No, it fails to compile. On the right side of ||, s is not definitely assigned — that branch runs precisely when the instanceof on the left was false, so the compiler cannot guarantee s was bound to anything there.

Object o = "test";
if (!(o instanceof String s)) {
    return;
}
System.out.println(s.length());
Object o = 42;
if (o instanceof String s || s.isEmpty()) {
    System.out.println(s);
}