instanceof and Pattern Matching in Java

instanceof tests the runtime type, and since Java 16 it can bind a typed variable in the same step.

The classic form

Object value = "Java notes";

if (value instanceof String) {
    String text = (String) value;      // the cast repeats what we just tested
    System.out.println(text.length());
}

instanceof asks whether the object on the left is an instance of the type on the right, including subtypes and implemented interfaces. It returns false for null, never an exception, which makes an explicit null check unnecessary.

Pattern matching for instanceof

if (value instanceof String text) {     // Java 16 and later
    System.out.println(text.length());
}

The type pattern declares text and assigns it only when the test succeeds. The cast disappears, and so does the chance of casting to the wrong type by mistake.

Scope of the pattern variable

if (value instanceof String text && text.length() > 3) {
    System.out.println(text.toUpperCase());     // in scope here
}

if (!(value instanceof String text)) {
    return;                                     // not in scope inside
}
System.out.println(text.length());              // in scope from here onwards

The variable is in scope exactly where the compiler can prove the test passed. The second form works with an early return and keeps the main path flat.

A practical example

static String render(Object value) {
    if (value instanceof Integer count) {
        return "count: " + count;
    }
    if (value instanceof List<?> items) {
        return "list of " + items.size();
    }
    if (value instanceof String text && !text.isBlank()) {
        return "text: " + text.strip();
    }
    return "unsupported";
}

Record patterns

record Point(int x, int y) { }
record Line(Point start, Point end) { }

static String describe(Object shape) {
    if (shape instanceof Line(Point(var x1, var y1), Point(var x2, var y2))) {
        return "from " + x1 + "," + y1 + " to " + x2 + "," + y2;
    }
    return "not a line";
}

Since Java 21 a pattern can destructure a record, binding its components directly and nesting to any depth. This removes a chain of accessor calls and makes the shape of the data visible in the code.

Combining with switch

static double area(Shape shape) {
    return switch (shape) {
        case Circle c    -> Math.PI * c.radius() * c.radius();
        case Square s    -> s.side() * s.side();
        case Rectangle r -> r.width() * r.height();
    };
}

When Shape is a sealed interface, the compiler knows every permitted subtype and accepts the switch without a default. Adding a new subtype then becomes a compile error at every switch that has not been updated, which is exactly the safety you want.

instanceof compared with getClass

instanceofgetClass() ==
Subtypes matchYesNo
Interfaces matchYesNo
null handlingReturns falseThrows NullPointerException
Typical useGeneral type testsStrict equals implementations

When type testing is a smell

// Every new type means editing this method
if (shape instanceof Circle c)    { return circleArea(c); }
if (shape instanceof Square s)    { return squareArea(s); }

// Better when you own the hierarchy: let each type answer for itself
interface Shape { double area(); }

Prefer polymorphism when you control the types. Pattern matching is the right tool when you do not, when the hierarchy is sealed and deliberately closed, or when the logic genuinely belongs outside the types.

Generics and erasure

// if (value instanceof List<String>)    // compile error
if (value instanceof List<?> items) { }  // allowed

Generic type arguments are erased at runtime, so only the unbounded wildcard form can be tested.

Common mistakes

  • Adding a redundant value != null check before instanceof.
  • Testing against a supertype first, so later branches are unreachable.
  • Using long instanceof chains where an overridden method would be cleaner.
  • Expecting instanceof List<String> to compile.
  • Casting after the test in modern Java, when the pattern variable already exists.

Best practices

  • Use the pattern form; it is shorter and removes a whole class of cast errors.
  • Order tests from most specific to most general.
  • Combine a sealed hierarchy with a pattern switch so the compiler enforces exhaustiveness.
  • Reach for polymorphism first when the types are yours to change.

Practice

  1. Rewrite a test and cast pair using a type pattern.
  2. Why does null instanceof String return false instead of throwing?
  3. Write a method that formats Integer, String and List differently using pattern matching.
  4. Convert an instanceof chain over a sealed interface into a pattern switch, and remove the default.
  5. Explain why the negated form if (!(o instanceof X x)) return; still leaves x usable afterwards.

Conclusion

Modern instanceof tests and binds in one step, record patterns destructure data, and a sealed hierarchy makes a pattern switch exhaustive. Together they replace most casting with something the compiler can verify.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Java notes →

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.