Command Palette

Search for a command to run...

PHASE 11Intermediate Java 16+ ~33 min· topic 1 of 6

Topic 11.1

Pattern Matching for instanceof

In one line

Pattern matching for instanceof tests a value's type and, if it matches, gives you a ready-cast variable in the same step: if (o instanceof String s) { s.length(); }. It removes the repeated cast, and the compiler decides exactly where the new variable can be used.

Think of it like this

A lost-and-found desk. Before, the clerk would look at a bag, say "this is a school bag", then walk to a shelf, pick up a "school bag" label, and stick it on before handing it over. Now the clerk checks the bag and it comes back already labelled in one move. If it isn't a school bag, nothing is labelled and nobody can call it one by mistake.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

instanceof
An operator that answers "is this object of this type (or a subtype)?" with true or false. It is always false for null.
Cast
Writing (Type) value to view a reference as a more specific type. The JVM checks it at run time and throws ClassCastException if it's wrong.
Pattern
A small description of a shape a value might have, like String s. Testing a value against it either matches (and fills in variables) or doesn't.
Type pattern
A pattern made of a type and a name, like String s. It matches when the value is a non-null instance of that type.
Pattern variable (binding)
The variable a pattern declares, like s in o instanceof String s. It is assigned only when the match succeeds.
Flow scoping
The rule that a binding exists only in the parts of the code the compiler can prove run after a successful match.
Definitely matched
The compiler's term for a place in the code that can only be reached if the pattern matched.
Shadowing
When a local variable has the same name as a field, so inside its scope the plain name means the local variable, not the field.

Step by step

01The old three-step dance

Before Java 16 you checked the type, then cast, then gave the result a name. The type String is written three times, and the cast is a separate statement you could get wrong: test for String but cast to Integer, and the compiler happily lets you, then the program throws ClassCastException.

Copy-paste makes it worse. In a chain of else if tests it is easy to move a cast into the wrong branch.

Main.javawhole filejava
Object o = "samosa";
if (o instanceof String) {          // 1. test
    String s = (String) o;          // 2. cast, 3. name
    System.out.println(s.length()); // 6
}

02One step: test and bind

With a type pattern, the test declares the variable: o instanceof String s. When the test is true, s already holds the same object, typed as String. No second statement, no chance of casting to the wrong type.

The object isn't copied or converted. s and o are two references to the same object on the heap; s just has the more specific type, so the compiler lets you call String methods on it.

Main.javawhole filejava
Object o = "samosa";
if (o instanceof String s) {        // test + cast + name, in one
    System.out.println(s.length()); // 6
}
One step: test and binddiagram
Rendering diagram…

03Flow scoping: where the binding exists

The compiler tracks where the match is definitely true. In a && b, b only runs if a was true, so a binding from a is usable in b. In a || b, b runs exactly when a was false, so the binding is not there.

Negation flips it. if (!(o instanceof String s)) { return; } means "if it's not a String, leave". Everything after that if is reached only when the match succeeded, so s is in scope for the rest of the block. This early-return style keeps the main logic unindented. It works only if the if body cannot complete normally (it must return, throw, break or continue).

Main.javawhole filejava
static boolean isLongWord(Object o) {
    return o instanceof String s && s.length() > 5;   // OK: s exists after &&
}

static String shout(Object o) {
    if (!(o instanceof String s)) {
        return "can't shout " + o;                    // s does not exist here
    }
    return s.toUpperCase() + "!";                     // OK: only reached on a match
}

// if (o instanceof String s || s.isEmpty())  // error: cannot find symbol s

04null is never a match

instanceof has always returned false for null, and a type pattern keeps that rule. So a binding is never null, and code inside the if never needs a null check for it.

That makes the modern equals idiom both short and safe: return o instanceof Money m && paise == m.paise && currency.equals(m.currency);. If o is null or another type, the whole expression is false before any field is read.

05What the compiler refuses

Impossible tests are errors: if x is declared Integer, x instanceof String s can never be true, and javac reports incompatible types: Integer cannot be converted to String.

Unsafe generic tests are errors too. At run time a List<String> is just a List (type erasure, Topic 8.6), so the JVM couldn't check the <String> part. From an Object, o instanceof List<String> list fails to compile; write o instanceof List<?> list instead. If the static type already proves the type argument (for example a Collection<String>), c instanceof List<String> l is allowed.

Patterns that always match were errors in Java 16 to 20 (String text; text instanceof String t) because the test is pointless. Java 21 relaxed that rule, and JDK 21's javac reports the old limit like this:

terminal
$ javac --release 17 Main.java
── expected output ──
Main.java:4: error: unconditional patterns in instanceof are not supported in -source 17
if (text instanceof String t) System.out.println(t);
^
(use -source 21 or higher to enable unconditional patterns in instanceof)
1 error

06The binding is a normal local variable

Once bound, s lives in the method's stack frame like any other local. It is not implicitly final (an early preview made it final, and Java 16 removed that rule), so n = n * 2; compiles. Write o instanceof final Integer n when you want the compiler to stop reassignment.

A binding can have the same name as a field. Inside its scope the name means the binding; outside, it means the field again. Code that relies on that switch is legal and hard to read, so avoid it.

07What javac really generates

There's no new bytecode instruction for patterns. The compiler emits instanceof, a conditional jump, checkcast and a store into a local slot: exactly what you'd write by hand. The checkcast can't fail here, and the JIT removes such redundant checks, so pattern matching costs nothing extra.

terminal
$ javac Shapes.java
javap -c Shapes
── expected output ──
static int len(java.lang.Object);
Code:
0: aload_0
1: instanceof #7 // class java/lang/String
4: ifeq 17
7: aload_0
8: checkcast #7 // class java/lang/String
11: astore_1
12: aload_1
13: invokevirtual #9 // Method java/lang/String.length:()I
16: ireturn
17: iconst_m1
18: ireturn

Try it yourself

  1. 1

    Predict null

    In the first example, add "" (an empty string) and 'x' (a Character) to the things array. Predict the two new lines before running. Then call shout(null) and predict whether it throws.

  2. 2

    Break the scope rule

    In isLongWord, change && to || and compile. Read the error, and explain in one sentence why s can't exist on the right of ||.

  3. 3

    Remove the early exit

    In shout, replace return "can't shout " + o; with System.out.println("can't shout"); (no return). Predict what the compiler says about the last line, then check.

Code & diagrams

Old way, new way, && and early return Java 16+ New tab
Sign in to run this example in your browser.

Expected output

text of length 6 | text of length 6
not text | not text
not text | not text
isLongWord("samosa"): true
isLongWord("tea"): false
isLongWord(null): false
CHAI!
can't shout 3.5
The one-line equals Java 16+ New tab

Money is final, so instanceof is a safe type check for equals (see Topic 4.8 on symmetry with subclasses).

Sign in to run this example in your browser.

Expected output

INR 10.50 equals INR 10.50? true
INR 10.50 equals USD 10.50? false
equals(null)? false
equals("INR 10.50")? false
distinct amounts: 1
Scope, shadowing and reassignment Java 16+ New tab

Shadowing a field with a binding is legal but confusing. It's shown here so you recognise it, not as a style to copy.

Sign in to run this example in your browser.

Expected output

inside if: pattern
after if: field
inside else: field
after if: field
doubled: 42
final binding: 21
Generic types: test with a wildcardjava
Object o = List.of("a", "b");

// if (o instanceof List<String> list)   // error: Object cannot be safely cast to List<String>
if (o instanceof List<?> list) {         // OK: checks only "is it a List?"
    for (Object item : list) {
        if (item instanceof String s) {  // check each element's real type
            System.out.println(s.toUpperCase());
        }
    }
}

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Use the binding after ||

Write if (o instanceof String s || s.isEmpty()) return 0;.

terminal
$ javac Main.java
── what you'll see ──
Main.java:3: error: cannot find symbol
if (o instanceof String s || s.isEmpty()) return 0;
^
symbol: variable s
location: class Main
1 error

Break #2

Test a generic type you can't check

With Object o = List.of("a");, write if (o instanceof List<String> list).

terminal
$ javac Main.java
── what you'll see ──
Main.java:5: error: Object cannot be safely cast to List<String>
if (o instanceof List<String> list) System.out.println(list);
^
1 error

Break #3

Use the binding after a negated test that falls through

Write if (!(o instanceof String s)) { System.out.println("no"); } and then System.out.println(s.length()); below it.

terminal
$ javac Main.java
── what you'll see ──
Main.java:7: error: cannot find symbol
System.out.println(s.length());
^
symbol: variable s
location: class Main
1 error

Myth vs fact

Myth

Pattern matching is slower than a cast because it does more.

Fact

It compiles to the same instanceof and checkcast bytecode as the hand-written version. There's no runtime difference.

Myth

The binding can be null if the object is null.

Fact

instanceof is false for null, so the binding is assigned only for a non-null object. Inside the matched scope it is never null.

Myth

A pattern variable is final.

Fact

Not since Java 16. It's an ordinary local you can reassign. Write instanceof final Type t if you want it final.

Myth

The binding is in scope for the whole if/else statement.

Fact

Only where the match is definitely true: the then branch (or the else branch of a negated test), the right of &&, and after an if that always exits on a failed match.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    JLS 6.3.1 defines the scoping rules as sets of variables "introduced when true" and "introduced when false" for each expression. ! swaps the sets, && carries the when-true set to its right operand, || carries the when-false set, and an if statement whose then branch can't complete normally introduces the when-false set into the following statements.

  • ▸

    Before Java 16, equals methods that used getClass() != o.getClass() were common partly because the instanceof-then-cast form was verbose. With a pattern, instanceof equals is one line, but the symmetry concern of Topic 4.8 still applies: use it in final classes or records, or when subclasses must not add state.

  • ▸

    Pattern matching does not make ClassCastException impossible in general, but it removes the most common cause: a cast that doesn't match the preceding test. Static analysis tools (IntelliJ, Error Prone) flag the old test-then-cast idiom and offer an automatic rewrite.

  • ▸

    Java 21 removed two Java 16 restrictions: an unconditional pattern (e.g. String s on a String) is now allowed in instanceof, and patterns may nest (o instanceof Point(int x, int y), Topic 11.3). Primitive types in patterns (o instanceof int i) are still a preview feature as of Java 25.

Remember this

  1. 1

    The old idiom has three steps that repeat the type: if (o instanceof String) { String s = (String) o; ... }. The new form, o instanceof String s, is a type pattern: String s is the pattern and s is a pattern variable (also called a binding). If o is a non-null String, the test is true and s holds o already typed as String. If not, the test is false and s is never assigned. It was previewed in Java 14 and 15 and became standard in Java 16 (JEP 394).

  2. 2

    The binding's scope follows the logic, not the braces. The compiler gives s a scope only where the match is definitely true: inside the if block, on the right side of && (o instanceof String s && s.length() > 5), and after an if whose negated test always leaves the method (if (!(o instanceof String s)) return; s.length();). This is called flow scoping. On the right side of ||, or in the else branch of a positive test, s doesn't exist and you get cannot find symbol.

  3. 3

    **null never matches.** null instanceof String s is false, so the binding is never null and you get a free null check. That is why return o instanceof Money m && amount == m.amount; is a complete, null-safe start to an equals method (Topic 4.8).

  4. 4

    The compiler still rejects tests that can never succeed (Integer vs String gives incompatible types) and tests that can't be checked at run time because of erasure: o instanceof List<String> list from an Object fails with Object cannot be safely cast to List<String> (Topic 8.6). From Java 21, a pattern that always matches, like String s on a String expression, is allowed; Java 16 to 20 rejected it.

  5. 5

    A pattern variable is an ordinary local variable once bound. Since Java 16 it is not implicitly final: you can reassign it, or declare o instanceof final String s to forbid that. It can shadow a field with the same name inside its scope, which is legal but confusing, so give bindings short, distinct names.

  6. 6

    Under the hood nothing new happens at run time. javac compiles o instanceof String s to the same instanceof, checkcast and astore bytecode you'd get from the hand-written version, so there is no performance cost. This is the first of Java's patterns; Topic 11.2 uses the same type patterns in switch, and Topic 11.3 adds record patterns that take objects apart.

Explain it without notes

01

What problem does pattern matching for instanceof solve, and when did it become standard?

02

Explain flow scoping with &&, || and an early return.

03

Why can the binding never be null?

04

Why does o instanceof List<String> l fail to compile when o is an Object?

05

What bytecode does o instanceof String s compile to, and is there a performance cost?

Practice

01

Write static int lengthOf(Object o) that returns the length of a String, the size of a java.util.Collection, the length of an int[], and -1 otherwise. Test it with "chai", List.of(1, 2, 3), new int[5] and 3.14.

02

Write static String firstWord(Object o) that returns "none" early if o is not a String or is blank, and otherwise returns the first word. Use a negated pattern with early return. Test with " hello world", " " and 42.

03

Write a final class Temperature with a double celsius field and an equals that uses a type pattern and Double.compare. Print new Temperature(36.6).equals(new Temperature(36.6)), .equals(null) and .equals("36.6").

Trade-offs

  • ↔

    Type patterns make type tests cheap to write, which also makes it easy to scatter instanceof chains through code. If you keep asking "are you a Dog? are you a Cat?", consider an overridden method (Topic 5.4) or a sealed type with an exhaustive switch (Topic 11.2).

  • ↔

    Early-return negated patterns (if (!(o instanceof T t)) return;) keep the main path flat, but the double negation is harder to read for beginners. For short methods a positive if block is clearer.

  • ↔

    instanceof-based equals is short and null-safe, but allows a subclass instance to equal a parent instance. Use it in final classes and records; use getClass() comparison when subclasses with extra state are expected.

Done when you can

  • Done when you can rewrite a test-then-cast block as a type pattern.

  • Done when you can say exactly where a binding is in scope with &&, ||, ! and early return.

  • Done when you can explain why the binding is never null.

  • Done when you know which tests the compiler rejects: impossible types and unverifiable generic types.

  • Done when you can write a one-line, null-safe equals with a type pattern.