PrepZone Logo
PrepZone

Pattern Matching for instanceof

Test, cast and bind in one step — and the guarded patterns that remove nested conditionals.

Read these first

Why this matters

  • The test-then-cast pair appears in almost every equals method and every piece of code handling a heterogeneous input, and it was always redundant.
  • The scoping rules for the bound variable are precise and occasionally surprising, which makes them a reliable interview question.
  • This is the first step of a larger feature: the same pattern syntax extends to switch and to record deconstruction.

Before and after

Java
// Java 8 — the type is written twice and the cast can drift from the test
if (object instanceof String) {
    String text = (String) object;
    if (text.length() > 3) {
        System.out.println(text.toUpperCase());
    }
}
Java
// Java 16+ — one test, one binding, no cast
if (object instanceof String text && text.length() > 3) {
    System.out.println(text.toUpperCase());
}
Before Java 16
1. instanceof check
2. Explicit cast
3. Assign to a variable
Java 16 and later
o instanceof String sTest, cast and bind in one step

s is in scope only where the test passed.

The type test and the binding happen together, so the variable is already the right type inside the branch and there is no cast left to get wrong.

The variable text is the pattern variable. It exists only where the pattern is known to have matched, and the compiler enforces that.

Where the pattern variable is in scope

This is the part worth reading carefully.

Java
// In the body of the if, when the pattern matched
if (object instanceof String text) {
    System.out.println(text.length());         // in scope
}
// System.out.println(text);                   // compile error — out of scope here
Java
// After && — because reaching the right operand means the pattern matched
if (object instanceof String text && text.isBlank()) { }

// After || — illegal, because the right operand runs when the pattern did NOT match
// if (object instanceof String text || text.isBlank()) { }   // compile error
Java
// In the else branch of a negated pattern — the useful guard-clause form
if (!(object instanceof String text)) {
    return 0;                                  // text is not in scope here
}
return text.length();                           // but it IS in scope for the rest of the method
Java
// Scope extends to the end of the enclosing block after an early return
void process(Object input) {
    if (!(input instanceof Order order)) {
        throw new IllegalArgumentException("not an order");
    }
    // order is usable for the whole rest of the method — no nesting, no cast
    audit(order);
    ship(order);
}

That guard-clause shape is the single biggest readability win in practice, because it removes a level of nesting from every method that validates its input.

Guarded patterns

A pattern combined with a condition using && is often called a guarded pattern. It collapses nested conditionals into one line.

Java
// Nested
if (object instanceof Order) {
    Order order = (Order) object;
    if (order.total().compareTo(THRESHOLD) > 0) {
        if (order.customer() != null) {
            flagForReview(order);
        }
    }
}

// Flat
if (object instanceof Order order
        && order.total().compareTo(THRESHOLD) > 0
        && order.customer() != null) {
    flagForReview(order);
}

equals, rewritten

Almost every hand-written equals becomes shorter.

Java
// The traditional form
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Point other = (Point) o;
    return x == other.x && y == other.y;
}

// With a type pattern
@Override
public boolean equals(Object o) {
    return o instanceof Point other && x == other.x && y == other.y;
}

instanceof also returns false for null, which means the null check is already handled — one more line the pattern form removes.

Java
Object nothing = null;
System.out.println(nothing instanceof String text);      // false, no exception

Combining with sealed types

Java
sealed interface Event permits Click, KeyPress, Scroll { }
record Click(int x, int y) implements Event { }
record KeyPress(char key) implements Event { }
record Scroll(int delta) implements Event { }
Java
String describe(Event event) {
    if (event instanceof Click click) return "click at " + click.x() + "," + click.y();
    if (event instanceof KeyPress press) return "key " + press.key();
    if (event instanceof Scroll scroll) return "scroll " + scroll.delta();
    throw new IllegalStateException();              // still needed with if-chains
}

A chain of if statements cannot be checked for exhaustiveness, so the unreachable throw remains. Moving to switch removes it — which is exactly why pattern matching for switch followed.

Java
String describe(Event event) {
    return switch (event) {
        case Click click -> "click at " + click.x() + "," + click.y();
        case KeyPress press -> "key " + press.key();
        case Scroll scroll -> "scroll " + scroll.delta();
    };                                              // exhaustive — no default, no throw
}
AspectPattern in ifPattern in switch
Available sinceJava 16Java 21
ExhaustivenessNot checked — needs a final throwChecked against a sealed hierarchy
Best forOne or two cases, guard clauses, equalsThree or more alternatives
NullFalls through as falseCan be matched with case null
ShapeImperative — do something and returnExpression — produce a value
  • Available since

    Pattern in ifJava 16
    Pattern in switchJava 21
  • Exhaustiveness

    Pattern in ifNot checked — needs a final throw
    Pattern in switchChecked against a sealed hierarchy
  • Best for

    Pattern in ifOne or two cases, guard clauses, equals
    Pattern in switchThree or more alternatives
  • Null

    Pattern in ifFalls through as false
    Pattern in switchCan be matched with case null
  • Shape

    Pattern in ifImperative — do something and return
    Pattern in switchExpression — produce a value

Use if for a guard clause or a single test; use switch once you are dispatching over a hierarchy.

Keeping it readable

Java
// Clear: the name says what was matched
if (payload instanceof JsonObject json && json.has("id")) { }

// Unclear: a short meaningless name loses the benefit
if (payload instanceof JsonObject j && j.has("id")) { }

Common misreadings

  • "The pattern variable is visible after the if." Only where the compiler can prove the match succeeded.
  • "It works after ||." It does not, because the right operand runs when the match failed.
  • "You still need a null check." instanceof is already false for null.
  • "It matches the exact class." It matches any subtype, so it is not a drop-in replacement for getClass() in an extensible class's equals.
  • "An if-chain over a sealed type is exhaustive." Only switch gets exhaustiveness checking.
  • "The pattern variable is final." It is effectively final by convention but can be reassigned; doing so is confusing and should be avoided.
  • "This is just syntax sugar." It is, and that is the point: less code that can drift out of sync.

Quick recall

Everything you need if you only revisit this box.

  • A type pattern combines test, cast and binding: if (o instanceof String s).
  • The pattern variable is in scope exactly where the match is provably true — in the if body, after &&, and after an early return in a negated test.
  • It is not in scope after ||.
  • instanceof is false for null, so no extra null check is needed.
  • The negated guard-clause form (if (!(o instanceof T t)) return;) extends the binding to the rest of the method and removes nesting.
  • A guarded pattern adds conditions with &&, flattening nested if blocks.
  • equals shrinks to one line — but keep getClass() for non-final classes, since patterns match subtypes.
  • if-chains are not exhaustiveness-checked; switch over a sealed type is.

Test yourself

Answer these before moving on — recall is what makes it stick.