Pattern Matching for instanceof
Test a type and bind a typed variable in one step, and use flow scoping to read loosely typed data and exception causes safely.
if (obj instanceof Type name) { /* name has type Type here */ }
if (!(obj instanceof Type name)) return; // name is in scope from here onBefore Java 16, checking a type and using it took two steps and a cast:
if (value instanceof String) {
String s = (String) value;
// use s
}
A type pattern does both at once. value instanceof String s tests the type and, when it matches, binds the value to a new variable s that already has type String. There is no cast to get wrong and no second name to keep in sync.
Where the variable is in scope
A pattern variable exists only where the compiler can prove the match succeeded. This is called flow scoping, and it gives three useful shapes:
- Inside the
ifblock:if (o instanceof Integer n) { ... }. - On the right of
&&:o instanceof String s && !s.isBlank(). - After a negated test that returns: following
if (!(o instanceof Map<?, ?> m)) return error;the variablemis usable for the rest of the method.
It is not in scope on the right of o instanceof String s || ..., because the left side may have failed. instanceof is always false for null, so a pattern never binds null and you need no separate null check.
Back-end uses
JSON parsed without a schema arrives as Map<String, Object>, holding strings, numbers, booleans, lists and nested maps. Patterns make reading such data short and safe: a missing key and a value of the wrong type fail the same test. They also tidy up error handling, for example walking a cause chain to find the SQLException under a framework's wrapper and reading its SQL state to choose between 409 and 500. And they shrink equals to a single expression.
Mistakes and limits
Generic type arguments are erased at runtime, so o instanceof List<String> list does not compile when o is an Object. Match List<?> and check the elements yourself.
A long chain of if (x instanceof A a) ... else if (x instanceof B b) is a sign you want a switch with patterns, which can also check that every case is covered. If you own the types, a method on the types themselves (plain polymorphism) is often better than asking each object what it is.
Since Java 21, instanceof also accepts record patterns, which pull a record's components out in the same step. That is the next lesson.
Example
When to use it
- Read fields from a JSON body parsed into Map<String, Object> and reject wrong types with a 400.
- Walk an exception's cause chain to turn a unique-constraint violation into a 409 Conflict.
- Write a short, null-safe equals method for a value class used as a map key.
More examples
equals in one expression
The pattern handles null, a different type and the cast in one expression. Because the class is final, instanceof cannot let a subclass break the symmetry of equals.
final class Sku {
private final String code;
Sku(String code) {
this.code = code.toUpperCase(Locale.ROOT);
}
@Override
public boolean equals(Object o) {
return o instanceof Sku other && code.equals(other.code);
}
@Override
public int hashCode() {
return code.hashCode();
}
}
void main() {
IO.println(new Sku("book-1").equals(new Sku("BOOK-1")));
IO.println(new Sku("book-1").equals("BOOK-1"));
IO.println(new Sku("book-1").equals(null));
}
// Output:
// true
// false
// falseValidating a loosely typed request body
Because the test is negated, qty is in scope on the right of || and both variables stay in scope after the early returns. A missing key returns null from get, which fails instanceof exactly like a wrong type.
String addToCart(Map<String, Object> body) {
if (!(body.get("sku") instanceof String sku)) {
return "400 sku must be a string";
}
if (!(body.get("quantity") instanceof Integer qty) || qty < 1) {
return "400 quantity must be a positive whole number";
}
return "201 added " + qty + " x " + sku;
}
void main() {
IO.println(addToCart(Map.of("sku", "BOOK-1", "quantity", 2)));
IO.println(addToCart(Map.of("sku", "BOOK-1", "quantity", "two")));
IO.println(addToCart(Map.of("quantity", 2)));
}
// Output:
// 201 added 2 x BOOK-1
// 400 quantity must be a positive whole number
// 400 sku must be a stringFinding the database error under wrappers
Frameworks wrap driver exceptions, so the useful SQLException is usually a few causes down. The pattern binds it with the right type so its SQL state can be read without a cast.
import java.sql.SQLException;
int statusFor(Throwable error) {
for (Throwable t = error; t != null; t = t.getCause()) {
// 23505 is PostgreSQL's SQL state for a unique constraint violation.
if (t instanceof SQLException sql && "23505".equals(sql.getSQLState())) {
return 409;
}
if (t instanceof IllegalArgumentException) {
return 400;
}
}
return 500;
}
void main() {
var duplicate = new SQLException("duplicate key value violates unique constraint", "23505");
var wrapped = new IllegalStateException("save failed", new RuntimeException("repository error", duplicate));
IO.println(statusFor(wrapped));
IO.println(statusFor(new IllegalArgumentException("bad id")));
IO.println(statusFor(new IllegalStateException("disk full")));
}
// Output:
// 409
// 400
// 500
Discussion