Skip to content
Chapter 48 · Core Java

Java 16: Pattern Matching for instanceof ​

Before Java 16, checking whether an object was a certain type and then using it as that type required three separate steps every single time. You wrote a check, you wrote a cast, and then you wrote the actual logic you cared about. It was repetitive, noisy, and honestly a little embarrassing given how smart Java compilers are. Java 16 fixed this with pattern matching for instanceof, one of those small additions that immediately makes your code feel cleaner and safer.

The Old Way and Its Boilerplate Problem ​

Think about what happens when you have a variable of type Object and you want to work with it as a String. You cannot just call string methods on it because the compiler only knows it is an Object. So you have to prove to the compiler that it is a String first, and then do an explicit cast.

Here is what that looked like before Java 16:

java
Object obj = "Hello, Java!";

if (obj instanceof String) {
    // We know it is a String here, but the compiler forgot
    String s = (String) obj;   // explicit cast required
    System.out.println(s.toUpperCase());
}

Look at that middle line. You just told the compiler in the if condition that obj is a String. The compiler accepted that check. And yet on the very next line you still have to cast it manually. The compiler did not carry that knowledge forward into the block. You do the same work twice: once to check, once to cast.

This becomes especially messy when you have multiple types to handle:

java
Object obj = getSomeObject();

if (obj instanceof String) {
    String s = (String) obj;
    System.out.println("String of length: " + s.length());
} else if (obj instanceof Integer) {
    Integer i = (Integer) obj;
    System.out.println("Integer value: " + i);
} else if (obj instanceof Double) {
    Double d = (Double) obj;
    System.out.println("Double value: " + d);
}

Every branch has that same redundant cast line. The check and the cast say exactly the same thing but in different syntax. This is pure boilerplate. It adds lines without adding meaning, and every extra line is a line where a typo can live.

Pattern Matching Combines the Check and the Bind ​

Java 16 introduced a new form of the instanceof operator that does the check and the binding in a single expression. Instead of just writing obj instanceof String, you write obj instanceof String s. That little s at the end is called the binding variable or the pattern variable.

Here is what happens when you write that:

  1. The JVM checks whether obj is actually a String at runtime
  2. If that check passes, it automatically casts obj to String and stores the result in s
  3. s is immediately available to use inside the if block
java
Object obj = "Hello, Java!";

if (obj instanceof String s) {
    // s is already a String here, no cast needed
    System.out.println(s.toUpperCase());
}

Three lines of the old version collapse into one expression. The compiler carries your type knowledge forward. You checked, it bound, you use. Done.

The same multi type example from before becomes dramatically cleaner:

java
Object obj = getSomeObject();

if (obj instanceof String s) {
    System.out.println("String of length: " + s.length());
} else if (obj instanceof Integer i) {
    System.out.println("Integer value: " + i);
} else if (obj instanceof Double d) {
    System.out.println("Double value: " + d);
}

Every redundant cast is gone. Each branch tells you exactly what type it handles and gives you a clean, properly typed variable to work with. The compiler also ensures type safety here: it knows that inside the String branch, s is definitely a String, so calling s.length() is safe without any cast.

Where You Can Use the Binding Variable: Scope Rules ​

The binding variable s in obj instanceof String s is not available everywhere in your code. It has a specific scope determined by where the check can be proven to be true.

The most straightforward case is inside the if block:

java
if (obj instanceof String s) {
    System.out.println(s);   // fine, s is definitely a String here
}
// s is NOT available here

Outside the block, the check has not been proven, so s does not exist. The compiler will refuse to compile any attempt to use it there.

However, the rule is really "wherever the check is definitely true", not just "inside the curly braces". The compiler uses flow analysis to decide this, so the variable can also be in scope after an if that exits early:

java
if (!(obj instanceof String s)) {
    // obj is definitely NOT a String here, s is not available
    return;
}
// After the early return, s IS available here because we know
// the only way to reach this line is if the check passed
System.out.println(s.toUpperCase());

This early return pattern is quite elegant. If the object is not the right type, you exit early. After that guard, the compiler knows the check must have succeeded, so it makes s available for the rest of the method without needing an if block wrapping everything.

Using the Binding Variable in Conditions ​

One of the most useful things you can do is combine the pattern match with additional conditions using the && operator. The pattern variable becomes available immediately after the pattern match succeeds within the same if condition:

java
Object obj = 5;

if (obj instanceof Integer i && i < 10) {
    System.out.println("Small integer: " + i);
}

This works because of the short circuit behavior of &&. The left side obj instanceof Integer i is evaluated first. If that succeeds, i is now bound to the integer value. Then the right side i < 10 is evaluated using that bound variable. If the left side fails, the right side is never evaluated, so there is no problem.

The order matters critically here. You cannot flip it:

java
// This does NOT compile
if (i < 10 && obj instanceof Integer i) {
    // ERROR: i might not be bound yet
}

The compiler rejects this because i is used before it could possibly be bound. Pattern matching guarantees that i only exists after the check passes, and putting i < 10 before the check violates that guarantee.

Why OR Does Not Work with Pattern Variables ​

You might wonder what happens if you try to use a pattern variable with the || operator instead of &&:

java
// This compiles but the pattern variable is useless
if (obj instanceof Integer i || obj instanceof String s) {
    // Neither i nor s is available here!
}

With ||, there is no guarantee which branch made the condition true. If obj is an Integer, then i would be bound but s would not. If obj is a String, then s would be bound but i would not. Inside the block, the compiler cannot know which one succeeded, so it cannot make either variable available safely.

So neither i nor s is in scope inside the block; the check works like a normal instanceof. Even though you wrote the pattern syntax, it gives you no advantage here. Always use && when you want to use the bound variable in conditions.

Pattern Matching Works with Interfaces Too ​

Pattern matching is not limited to concrete classes. You can use it with interfaces, and the results are particularly elegant because of polymorphism.

Imagine you have an interface and two implementing classes:

java
interface Vehicle {
    void drive();
}

class TwoWheeler implements Vehicle {
    @Override
    public void drive() {
        System.out.println("Two-wheeler driving!");
    }
}

class FourWheeler implements Vehicle {
    @Override
    public void drive() {
        System.out.println("Four-wheeler driving!");
    }
}

The old way of working with this looked like:

java
Object obj = new TwoWheeler();

if (obj instanceof TwoWheeler) {
    TwoWheeler tw = (TwoWheeler) obj;
    tw.drive();
} else if (obj instanceof FourWheeler) {
    FourWheeler fw = (FourWheeler) obj;
    fw.drive();
}

With pattern matching and interfaces, you can check against the parent interface type and still get proper polymorphic behavior:

java
Object obj = new TwoWheeler();

if (obj instanceof Vehicle v) {
    v.drive();   // calls TwoWheeler's drive() implementation
}

When obj holds a TwoWheeler, the check obj instanceof Vehicle passes because TwoWheeler implements Vehicle. v has the type Vehicle (the cast is to Vehicle), but it still points to the TwoWheeler object, so calling v.drive() uses polymorphism and runs the TwoWheeler implementation. If obj held a FourWheeler, the same code would call the FourWheeler implementation instead.

This is cleaner than checking each subtype individually when you only need the interface methods.

Pattern Matching with Negation ​

Negating a pattern match is a common and important pattern. You want to say "if this is not a String, do something else." You can negate the entire check:

java
if (!(obj instanceof String s)) {
    System.out.println("Not a string, skipping");
    return;
}

// Here, s is available because the negation means we only
// reach this line when the instanceof check was true
System.out.println(s.length());

This guard pattern is very readable. The unhappy path is handled first and exited early. Everything after the guard can assume the check passed.

Note that inside the negated block itself (the if body when you write !(obj instanceof String s)), s is NOT available because inside that block the check has failed. The variable is only available where the check is guaranteed to have succeeded.

A Complete Working Example ​

Here is a comprehensive example putting all the concepts together:

java
public class PatternMatchingDemo {

    public static void describe(Object obj) {
        // Simple pattern match
        if (obj instanceof String s) {
            System.out.println("String: " + s.toUpperCase());
        }
        // Pattern match with additional condition using &&
        else if (obj instanceof Integer i && i > 0) {
            System.out.println("Positive integer: " + i);
        }
        // Pattern match catching all integers (negative and zero)
        else if (obj instanceof Integer i) {
            System.out.println("Non-positive integer: " + i);
        }
        // Pattern match with interface
        else if (obj instanceof Vehicle v) {
            System.out.println("Some vehicle:");
            v.drive();
        }
        else {
            System.out.println("Unknown type: " + obj.getClass().getName());
        }
    }

    public static void main(String[] args) {
        describe("hello");           // String: HELLO
        describe(42);                // Positive integer: 42
        describe(-5);                // Non-positive integer: -5
        describe(new TwoWheeler()); // Some vehicle: Two-wheeler driving!
    }
}

Notice how every branch is self contained and readable. Each one tells you the type it expects and gives you a properly typed variable to work with immediately.

When Was This Finalized? ​

Pattern matching for instanceof went through the Java preview process before being finalized. It was available as a preview feature in Java 14 and Java 15. In Java 16, it became a standard, stable feature. On Java 16 or later you can use it without any special compiler flags.

There is also pattern matching for switch expressions, which builds on these same ideas and became standard in Java 21. It is covered in its own chapter.

Check yourself ​

  1. What two steps does obj instanceof String s do?
  2. Does if (obj instanceof Integer i && i < 10) compile? What about if (i < 10 && obj instanceof Integer i)?
  3. In if (obj instanceof Integer i || obj instanceof String s) { ... }, can you use i or s inside the block?
  4. After if (!(obj instanceof String s)) return;, can you use s on the next line?
  5. obj holds a TwoWheeler. After obj instanceof Vehicle v, what is the type of v, and whose drive() runs?
  6. In which Java version was this finalized, and in which version did pattern matching for switch arrive?
Answers
  1. It checks the type, and if true, casts and assigns the value to s.
  2. The first compiles (&& evaluates left to right, so i is bound first). The second does not.
  3. No. The compiler cannot know which side was true.
  4. Yes. The only way to reach that line is if the check was true.
  5. v is a Vehicle; TwoWheeler's drive() runs because of polymorphism.
  6. Java 16 (preview in 14 and 15). Pattern matching for switch: Java 21.