Java Instanceof and Pattern Matching

Naveen AnamaneniUpdated · 6 min read

A payments screen receives a list of Payment objects. A UPI payment should show its UPI id, and a card payment the last four digits of the card. Through a Payment variable you can't see either. You first have to ask, "is this object a UPI payment?", and then treat it as one. The instanceof operator asks that question, and since Java 16, pattern matching for instanceof asks it and gives you a correctly typed variable in one step.

The Classic Way

Classic
if (p instanceof UpiPayment) {
    UpiPayment upi = (UpiPayment) p;   // test, then cast
    System.out.println(upi.upiId);
}

x instanceof Type is true when x refers to an object of Type or any subtype of it. It's false when x is null, so it never throws an exception. The cast on the next line repeats the type name, and if someone later changes one of the two types but not the other, the code breaks.

Pattern Matching

Payments.java▶ Try it
public class Payments {
    static class Payment {
        long amount;
        Payment(long amount) { this.amount = amount; }
    }
 
    static class UpiPayment extends Payment {
        String upiId;
        UpiPayment(long amount, String upiId) {
            super(amount);
            this.upiId = upiId;
        }
    }
 
    static class CardPayment extends Payment {
        String last4;
        CardPayment(long amount, String last4) {
            super(amount);
            this.last4 = last4;
        }
    }
 
    static String describe(Payment p) {
        if (p instanceof UpiPayment upi) {
            return "UPI " + upi.upiId + ": Rs. " + upi.amount;
        } else if (p instanceof CardPayment card && card.amount > 10000) {
            return "Large card payment, card ending " + card.last4;
        } else if (p instanceof CardPayment card) {
            return "Card ending " + card.last4 + ": Rs. " + card.amount;
        }
        return "Unknown payment";
    }
 
    public static void main(String[] args) {
        System.out.println(describe(new UpiPayment(1200, "navika@upi")));
        System.out.println(describe(new CardPayment(25000, "4321")));
        System.out.println(describe(new CardPayment(800, "9876")));
        System.out.println(describe(null));
    }
}
Output
UPI navika@upi: Rs. 1200
Large card payment, card ending 4321
Card ending 9876: Rs. 800
Unknown payment

How it works

  1. p instanceof UpiPayment upi tests the type and, if the test passes, declares upi already cast to UpiPayment. upi is called a binding variable (or pattern variable).
  2. p instanceof CardPayment card && card.amount > 10000: after &&, the test has passed, so card can be used in the same condition.
  3. describe(null): every instanceof test is false for null, so the method falls through to "Unknown payment" without a NullPointerException.

Try it yourself: add a CashPayment class and a branch for it. What does describe print for a cash payment before you add the branch?

Where the Binding Variable Exists

The compiler tracks where the test is definitely true, and makes the variable available only there. This is called flow scoping:

CodeIs the variable usable?
if (o instanceof String s) { s.length(); }Yes, inside the if block
if (o instanceof String s && s.length() > 3)Yes, after &&
if (o instanceof String s || s.isEmpty())No: after ||, the test may have failed
if (!(o instanceof String s)) return; s.length();Yes, after the if, because the method returned otherwise

The || case is a compile error:

Compiler output
Inst2.java:9: error: cannot find symbol
        if (o instanceof String u || u.isEmpty()) {}
                                     ^
  symbol:   variable u
  location: class Inst2
1 error

The negated form, if (!(o instanceof String s)) return;, is handy at the start of a method: it rejects the wrong type early and leaves s usable for the rest of the method.

Impossible Tests Don't Compile

If the compiler can prove a test could never be true, it rejects it:

Compiler output
Inst.java:4: error: incompatible types: Integer cannot be converted to String
        System.out.println(n instanceof String);
                           ^
1 error

An Integer variable can never refer to a String, because neither class extends the other.

Common Mistakes

Testing, then casting by hand. Use the pattern form; it's shorter and the type is written once.

Using the binding variable after || or outside the true branch. It's only in scope where the test is definitely true.

Checking obj != null && obj instanceof X. The null check is redundant: instanceof already returns false for null.

Replacing polymorphism with instanceof chains. Each new type then means editing every chain.

Interview Corner

What does instanceof return for null?

Always false. null isn't an instance of any type, so x instanceof T is false when x is null, and it never throws a NullPointerException.

What is pattern matching for instanceof?

Since Java 16, obj instanceof Type name tests the type and, when it matches, binds a new variable of that type, removing the separate cast. The binding variable uses flow scoping: it's available only where the compiler knows the test succeeded, such as inside the if block, after &&, or after an early return in the negated case.

Practice

  1. Easy: Write static String kind(Object o) that returns "text of length N" for a String, "number N" for an Integer, and "something else" otherwise.
  2. Harder: Rewrite describe from Payments.java so the Payment class has an overridable describe() method and the instanceof checks disappear. Which version is easier to extend?

Quick check: instanceof

4 questions
1What does null instanceof String evaluate to?
2In if (o instanceof String s && s.length() > 3), is s usable after &&?
3In if (o instanceof String s || s.isEmpty()), what happens?
4Since which version is pattern matching for instanceof standard?

Key Takeaways

Was this tutorial helpful?

Lesson 131 of 482