The redundant-cast problem
Before Java 16, checking a type and then using the value always required two steps:
Object obj = getObject();
if (obj instanceof String) {
String s = (String) obj; // we already know it's a String — why cast?
System.out.println(s.toUpperCase());
}
The cast (String) obj cannot fail here — we just proved obj is a String. It's pure
boilerplate, and it violates DRY: the type is stated twice (instanceof String and
(String)). A typo could cast to a different type than was tested, producing a runtime
ClassCastException instead of a compile error.
Pattern matching for instanceof (finalized in Java 16, JEP 394) solves this:
if (obj instanceof String s) {
System.out.println(s.toUpperCase()); // s is bound — no cast needed
}
One expression: test the type, bind the result. The variable s is typed, non-null, and
in scope exactly where the test is known to be true.
Flow-sensitive typing and scope rules
The binding variable is in scope only where the compiler can prove the pattern has matched — a property called flow-sensitive typing or definite assignment:
Object obj = getObject();
if (obj instanceof String s) {
// s is in scope here — pattern matched
System.out.println(s.length());
}
// s is NOT in scope here
// In the else branch, the pattern is known to be false:
if (obj instanceof String s) {
System.out.println(s);
} else {
// s is NOT in scope — the test failed
}
&& chains
The binding variable is in scope throughout an && chain because the left side must be
true before the right side is evaluated:
if (obj instanceof String s && s.length() > 5) {
System.out.println("long: " + s); // s available throughout the && chain
}
|| chains
The compiler cannot prove the type in an || chain, so the binding variable is not
in scope:
// Compile error:
if (obj instanceof String s || s.isEmpty()) { ... }
// ^ s not in scope — test might not have matched
Negation and guard clauses
When you negate the pattern (!(obj instanceof T t)), the binding variable is in scope
after an early-exit (return, throw) — the compiler knows that any code past the guard can
assume the test passed:
void process(Object obj) {
if (!(obj instanceof String s)) {
throw new IllegalArgumentException("Expected String, got: " + obj);
}
// s is in scope here — we know obj is a String
System.out.println(s.toUpperCase());
}
This is the guard clause pattern (fail fast, return early) applied to type checking.
It's often cleaner than a deep nested if/else block:
// Deeply nested — harder to read:
void handle(Object obj) {
if (obj instanceof Request req) {
if (req.isValid()) {
if (req.hasPermission()) {
process(req);
}
}
}
}
// Guard clauses — flat and readable:
void handle(Object obj) {
if (!(obj instanceof Request req)) return;
if (!req.isValid()) return;
if (!req.hasPermission()) return;
process(req);
}
instanceof and null
instanceof always returns false for null — this is unchanged from traditional
instanceof. Pattern matching inherits this: if the object is null, the pattern doesn't
match, and the binding variable is never bound.
Object obj = null;
System.out.println(obj instanceof String); // false
System.out.println(obj instanceof String s); // false — s never bound
if (obj instanceof String s) {
// We never enter this branch for null
s.length(); // no NPE risk — guaranteed non-null
}
This means you don't need a separate != null check before an instanceof pattern —
the null case is handled implicitly.
Improving equals() with pattern matching
The most common use of the old instanceof + cast was in equals() overrides. Pattern
matching makes them concise and eliminates the cast:
public final class Point {
final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
// Old way:
@Override
public boolean equals(Object o) {
if (!(o instanceof Point)) return false;
Point other = (Point) o; // redundant cast
return x == other.x && y == other.y;
}
// Pattern matching (Java 16+):
@Override
public boolean equals(Object o) {
return o instanceof Point other
&& x == other.x
&& y == other.y;
}
}
The pattern-matching version is a single expression — compact, readable, and impossible to accidentally cast to the wrong type.
Ternary expressions
Pattern binding variables work in the true branch of a ternary:
Object obj = getValue();
String result = obj instanceof String s ? s.toUpperCase() : "not a string";
The binding variable s is only accessible in the true branch, exactly where the test
is known to have passed.
Generic types — the erasure limitation
Because of type erasure, you can only test the raw type at runtime, not a
parameterized generic. Testing obj instanceof List<String> is a compile error:
Object obj = List.of("a", "b");
// Compile error — generic argument erased at runtime:
// if (obj instanceof List<String> strings) { }
// Correct — use wildcard:
if (obj instanceof List<?> list) {
list.forEach(System.out::println); // elements are Object
}
If you need to verify element types, check each element individually inside the branch.
Variable shadowing
Pattern binding variables follow the same shadowing rules as local variables. They can shadow a field from an outer scope but cannot shadow an existing local variable in the same scope:
class Processor {
String value = "field";
void run(Object obj) {
// Shadows the field 'value' — allowed:
if (obj instanceof String value) {
System.out.println(value); // pattern variable, not the field
}
String existing = "local";
// Cannot shadow a local variable in the same scope:
// if (obj instanceof String existing) { } // compile error
}
}
instanceof patterns vs switch patterns
Both belong to the same pattern matching feature family:
| Feature | Java version | Best for |
|---|---|---|
instanceof pattern | Java 16 | One or two type checks |
| Switch type pattern | Java 21 | Three or more types, exhaustiveness needed |
// One type — instanceof is cleanest:
if (event instanceof OrderPlaced e) {
handlePlaced(e);
}
// Multiple types — switch is cleaner and exhaustiveness-checked:
switch (event) {
case OrderPlaced e -> handlePlaced(e);
case OrderShipped e -> handleShipped(e);
case OrderCancelled e -> handleCancelled(e);
}
Use instanceof patterns for simple type guards and equals() overrides; graduate to
switch type patterns when dispatching over three or more types, especially sealed ones.
Old instanceof + cast is a code smell
In Java 16+ codebases, the old instanceof + cast on the next line is a code smell. It
should be replaced in code reviews:
// Code smell — Java 16+ projects:
if (event instanceof OrderPlaced) {
OrderPlaced placed = (OrderPlaced) event; // ← flag this in review
...
}
// Fix:
if (event instanceof OrderPlaced placed) {
...
}
Many IDEs (IntelliJ IDEA, Eclipse) offer automatic quick fixes to convert the old pattern to the new one across the entire codebase.
Recap
Pattern matching for instanceof (Java 16) eliminates the redundant cast that always
followed a type test. The binding variable is in scope only where the compiler can prove
the test passed — in the true branch and && chains, but not in || chains or the else
branch. Negated patterns work with guard clauses: after an early exit, the binding
variable flows into scope for the rest of the method. instanceof always returns false
for null, so bound variables are guaranteed non-null. The most impactful use is
simplifying equals() overrides into a single expression. Generic arguments cannot be
tested at runtime due to type erasure — use List<?> instead of List<String>. For
multi-way type dispatch, graduate to switch type patterns (Java 21), which add
exhaustiveness checking for sealed types.