Part 5 of 14

Pattern Matching for instanceof (JEP 394): Smarter Type Checks

Finalized in Java 16 (JEP 394). Available in all Java 16+ releases, including Java 17. Previous previews: Java 14 (JEP 305) and Java 15 (JEP 375).

The Problem: The instanceof-Cast Dance

Every Java developer has written this pattern:

// Java 11 — check, then cast
if (obj instanceof String) {
    String s = (String) obj;  // cast is logically redundant
    System.out.println(s.toUpperCase());
}

The instanceof check already verified the type. The cast on the next line is conceptually redundant — the compiler could infer that obj is a String in the if body. But Java 11 requires the cast anyway, and it has two problems:

  1. Verbosity: you name the variable twice (obj instanceof String then String s = (String) obj)
  2. Error-prone: if you change the type in instanceof but forget to update the cast, the code compiles but throws ClassCastException at runtime

Pattern Matching for instanceof eliminates both problems.


The Solution: Pattern Variables

// Java 17 — pattern variable binds the checked type in one step
if (obj instanceof String s) {
    System.out.println(s.toUpperCase());
}

s is a pattern variable: it is bound to obj cast to String if and only if the instanceof check succeeds. Inside the if block, s is in scope and can be used directly — no explicit cast needed.

If the check fails (obj is null or not a String), s is not bound and is not accessible.


Scope Rules

True Branch Scope

The pattern variable is in scope in the true branch of the if:

if (obj instanceof String s) {
    // s is in scope here
    System.out.println(s.length()); // ok
}
// s is NOT in scope here

Negation Scope

When the instanceof is negated, the pattern variable is in scope in the false branch:

if (!(obj instanceof String s)) {
    // s NOT in scope here
    throw new IllegalArgumentException("expected String");
}
// s IS in scope here — the check passed
System.out.println(s.length());

This is useful for guard clauses at the start of methods.

Logical AND Scope

The pattern variable is in scope on the right side of && (because && short-circuits: if instanceof fails, the right side is never evaluated):

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

This is idiomatic and very common. The && condition is evaluated only when obj is a String, so s is guaranteed to be non-null and correctly typed.

Logical OR Scope

Pattern variables are NOT in scope on the right side of || — because if instanceof fails but the right side succeeds, s would be unbound:

// Does NOT compile: s is not definitely assigned in the || case
if (obj instanceof String s || someOtherCondition) {
    System.out.println(s); // error
}

Combining with Records

Pattern matching for instanceof and records complement each other. Records generate consistent, type-safe accessors:

record Point(int x, int y) {}

void printCoordinate(Object obj) {
    if (obj instanceof Point p) {
        System.out.printf("Point at (%d, %d)%n", p.x(), p.y());
    }
}

This becomes even more powerful with Record Patterns in Java 21:

// Java 21 — record pattern deconstructs in one step
if (obj instanceof Point(int x, int y)) {
    System.out.printf("Point at (%d, %d)%n", x, y);
}

In Java 17, you use the intermediate p variable. The direction is clear though — records are designed to be deconstructed via patterns.


Replacing Complex instanceof Chains

Pattern matching cleans up multi-type dispatch code significantly:

// Java 11 — nested instanceof with casts
double area(Shape shape) {
    if (shape instanceof Circle) {
        Circle c = (Circle) shape;
        return Math.PI * c.radius() * c.radius();
    } else if (shape instanceof Rectangle) {
        Rectangle r = (Rectangle) shape;
        return r.width() * r.height();
    } else if (shape instanceof Triangle) {
        Triangle t = (Triangle) shape;
        return 0.5 * t.base() * t.height();
    }
    throw new UnsupportedOperationException("Unknown shape: " + shape);
}

// Java 17 — pattern variables eliminate casts
double area(Shape shape) {
    if (shape instanceof Circle c) {
        return Math.PI * c.radius() * c.radius();
    } else if (shape instanceof Rectangle r) {
        return r.width() * r.height();
    } else if (shape instanceof Triangle t) {
        return 0.5 * t.base() * t.height();
    }
    throw new UnsupportedOperationException("Unknown shape: " + shape);
}

In Java 21, this becomes a pattern switch expression (exhaustive, no throw needed):

// Java 21 — sealed + pattern switch
double area(Shape shape) {
    return switch (shape) {
        case Circle c    -> Math.PI * c.radius() * c.radius();
        case Rectangle r -> r.width() * r.height();
        case Triangle t  -> 0.5 * t.base() * t.height();
    };
}

Pattern Variables vs Local Variables

Pattern variables behave like local variables except they are bound by the condition:

if (obj instanceof String s) {
    String s = "other"; // compile error: variable s already defined
}

Pattern variables cannot shadow each other in nested conditions:

if (obj instanceof Number n) {
    if (obj instanceof Integer n) { // compile error: n already in scope
    }
}

Null Safety

null instanceof T is always false, regardless of T. Pattern variables are therefore always non-null:

Object obj = null;
if (obj instanceof String s) {
    // never reached — null instanceof String is false
    System.out.println(s); // s would be non-null if we got here
}

This is different from a cast: (String) null succeeds (yields null), but null instanceof String fails. Pattern matching is always null-safe.


Guard Clauses (Early Return Pattern)

Pattern matching for instanceof works cleanly in guard clauses:

void process(Object input) {
    if (!(input instanceof Request req)) {
        throw new IllegalArgumentException("Expected Request, got: " + input);
    }
    // req is in scope from here on
    handleRequest(req);
}
void process(Object input) {
    if (!(input instanceof String s) || s.isBlank()) {
        return; // not a non-blank string
    }
    // s is a non-blank String here
    System.out.println("Processing: " + s.strip());
}

This is the Java 17 equivalent of Kotlin’s ?: return or Scala’s pattern guards.


Using in Equals

Pattern matching for instanceof simplifies equals() implementations:

// Java 11
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof MyClass)) return false;
    MyClass other = (MyClass) o;
    return Objects.equals(name, other.name);
}

// Java 17
@Override
public boolean equals(Object o) {
    return this == o
        || o instanceof MyClass other
            && Objects.equals(name, other.name);
}

Note: && is used here — the pattern variable other is in scope on the right side of &&.


What’s Coming: Patterns Evolve in Java 21

Pattern Matching for instanceof (JEP 394) in Java 17 is the foundation. Java 21 extends it significantly:

FeatureJava 17Java 21
Pattern variable from instanceofYesYes
Pattern in switchPreview (JEP 406)Final (JEP 441)
Record patterns (deconstruction)NoFinal (JEP 440)
Nested patternsNoYes
Guarded patterns (when)NoYes

In Java 17, master the instanceof pattern — it is the building block for everything that follows.


Summary

ConceptExample
Basic pattern variableif (obj instanceof String s)
Pattern + conditionif (obj instanceof String s && s.length() > 3)
Negation scopeif (!(obj instanceof String s)) throw; // s in scope after
Null safetynull instanceof T is always false
Guard clauseif (!(obj instanceof Foo f)) return;
Equals simplificationo instanceof MyClass other && Objects.equals(field, other.field)

What’s Next

Article 6: Switch Expressions (JEP 361) covers the switch expression — using switch as a value-returning expression with arrow syntax and no fall-through.