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.

Syntaxif (obj instanceof Type name) { /* name has type Type here */ } if (!(obj instanceof Type name)) return; // name is in scope from here on

Before 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 if block: 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 variable m is 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

Try it yourself
Loading editor…
Press Run to execute the code.

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.

Example · java
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
// false

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

Example · java
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 string

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

Example · java
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

  • Be the first to comment on this lesson.