Contents
- Before and After
- Scope of the Binding Variable
- Negation and Flow Scoping
- Implementing equals() with Pattern Matching
- Combining with && and ||
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.