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:
- Verbosity: you name the variable twice (
obj instanceof StringthenString s = (String) obj) - Error-prone: if you change the type in
instanceofbut forget to update the cast, the code compiles but throwsClassCastExceptionat 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:
| Feature | Java 17 | Java 21 |
|---|---|---|
| Pattern variable from instanceof | Yes | Yes |
| Pattern in switch | Preview (JEP 406) | Final (JEP 441) |
| Record patterns (deconstruction) | No | Final (JEP 440) |
| Nested patterns | No | Yes |
Guarded patterns (when) | No | Yes |
In Java 17, master the instanceof pattern — it is the building block for everything that follows.
Summary
| Concept | Example |
|---|---|
| Basic pattern variable | if (obj instanceof String s) |
| Pattern + condition | if (obj instanceof String s && s.length() > 3) |
| Negation scope | if (!(obj instanceof String s)) throw; // s in scope after |
| Null safety | null instanceof T is always false |
| Guard clause | if (!(obj instanceof Foo f)) return; |
| Equals simplification | o 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.