Pattern Matching for instanceof (Java 16+)

Pattern matching for instanceof, standardized in Java 16 (JEP 394), eliminates the redundant explicit cast that always followed an instanceof check. A single expression both tests the type AND binds the result to a typed variable, making the code shorter, safer, and less error-prone.

Contents

Before and After

Before Java 16, every instanceof check required a separate cast on the next line: the cast was technically redundant because the type had just been verified, but the compiler required it. Pattern matching instanceof (JEP 394, standard in Java 16) combines both into a single expression: if (obj instanceof String s) tests the type, and if the test passes, binds the value to s as a String. The binding variable s is only in scope in the branch where the test is known to have succeeded; the compiler enforces this through flow-sensitive scoping.

Java
// BEFORE Java 16 — test + cast (redundant, error-prone)
if (obj instanceof String) {
    String s = (String) obj; // cast is redundant — we just checked
    System.out.println(s.toUpperCase());
}

// AFTER Java 16 — pattern variable bound in one step
if (obj instanceof String s) {
    System.out.println(s.toUpperCase()); // s is a String here
}

// Works for any type
Object value = getValue();
if (value instanceof List<?> list) {
    System.out.println("List with " + list.size() + " elements");
} else if (value instanceof Map<?,?> map) {
    System.out.println("Map with " + map.size() + " entries");
} else if (value instanceof Number n) {
    System.out.println("Number: " + n.doubleValue());
}
The pattern variable is implicitly final: you cannot reassign it within its scope, which prevents accidental mutation.

Scope of the Binding Variable

The binding variable is only in scope where the compiler can prove the instanceof test succeeded: this is called flow-sensitive scoping.

Java
Object obj = "Hello, World!";

// 's' is in scope inside the if-block (where instanceof is true)
if (obj instanceof String s) {
    System.out.println(s.length()); // OK — s is definitely a String here
}
// System.out.println(s.length()); // COMPILE ERROR — s out of scope

// 's' is also in scope in the condition itself after &&
if (obj instanceof String s && s.length() > 5) {
    System.out.println("Long string: " + s);
}

// The following is a compile error — s is not proven bound here
// if (obj instanceof String s || s.isEmpty()) { }  // ERROR

Negation and Flow Scoping

When you negate the instanceof test with !, the binding variable is in scope in the else-branch (or after an early return):

Java
// Early return pattern — binding variable in scope after guard
static void process(Object obj) {
    if (!(obj instanceof String s)) {
        throw new IllegalArgumentException("Expected String, got: " + obj.getClass());
    }
    // s is in scope here — the instanceof is guaranteed true
    System.out.println("Processing: " + s.toUpperCase());
    System.out.println("Length: " + s.length());
}

// Same idea with else-branch
static String describe(Object obj) {
    if (!(obj instanceof Number n)) {
        return "not a number";
    }
    // n is in scope from here on
    return "number: " + n.doubleValue();
}

Implementing equals() with Pattern Matching

One of the most common uses of instanceof before Java 16 was in equals(). Pattern matching makes this much cleaner:

Java
// Before — typical equals() boilerplate
@Override
public boolean equals(Object obj) {
    if (this == obj) return true;
    if (!(obj instanceof Point)) return false;
    Point other = (Point) obj;
    return this.x == other.x && this.y == other.y;
}

// After — cleaner with pattern matching
@Override
public boolean equals(Object obj) {
    return this == obj ||
           obj instanceof Point p &&
           this.x == p.x &&
           this.y == p.y;
}

// For more complex classes
public class Order {
    private final String id;
    private final double total;
    private final String customerId;

    @Override
    public boolean equals(Object obj) {
        return obj instanceof Order o &&
               Objects.equals(this.id, o.id) &&
               Double.compare(this.total, o.total) == 0 &&
               Objects.equals(this.customerId, o.customerId);
    }
}
Note: for value-object classes, consider using record instead. Records auto-generate a correct equals() based on all components.

Combining with && and ||

The binding variable introduced by a pattern can be used in the same && condition to the right of the test: if (obj instanceof String s && s.length() > 5) works because the compiler knows s is bound when that right operand is evaluated. Combining with || is not allowed because the left operand may be false, leaving s unbound. instanceof also implicitly handles null: a null reference never matches any type, so pattern matching instanceof is always null-safe with no extra check required.

Java
// && — binding variable available in right operand if test passed on left
if (obj instanceof String s && !s.isBlank() && s.startsWith("http")) {
    System.out.println("URL: " + s);
}

// Practical: null-safe type check
Object maybeNull = getResult(); // could be null
if (maybeNull instanceof String s) {
    // null never matches instanceof — this is null-safe
    System.out.println("Got string: " + s);
}

// instanceof returns false for null — no NullPointerException
Object nullRef = null;
System.out.println(nullRef instanceof String); // false (no NPE)

// Combining multiple patterns in a chain (pre-switch-expression style)
static double toDouble(Object value) {
    if (value instanceof Double d)  return d;
    if (value instanceof Float f)   return f.doubleValue();
    if (value instanceof Long l)    return l.doubleValue();
    if (value instanceof Integer i) return i.doubleValue();
    if (value instanceof String s)  return Double.parseDouble(s);
    throw new IllegalArgumentException("Cannot convert to double: " + value);
}
Pattern matching for instanceof is the building block for pattern matching in switch (Java 21+). Learn both: they complement each other for different use cases.