Contents

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.

// 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.

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

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

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

// 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(); }

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

// 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.

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.

// && — 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.