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
trueorfalse. It is alwaysfalsefornull. - Cast
- Writing
(Type) valueto view a reference as a more specific type. The JVM checks it at run time and throwsClassCastExceptionif 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
sino 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.
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.
Object o = "samosa";
if (o instanceof String s) { // test + cast + name, in one
System.out.println(s.length()); // 6
}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).
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 s04null 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:
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.
Try it yourself
- 1
Predict null
In the first example, add
""(an empty string) and'x'(aCharacter) to thethingsarray. Predict the two new lines before running. Then callshout(null)and predict whether it throws. - 2
Break the scope rule
In
isLongWord, change&&to||and compile. Read the error, and explain in one sentence whyscan't exist on the right of||. - 3
Remove the early exit
In
shout, replacereturn "can't shout " + o;withSystem.out.println("can't shout");(no return). Predict what the compiler says about the last line, then check.
Code & diagrams
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.5Money is final, so instanceof is a safe type check for equals (see Topic 4.8 on symmetry with subclasses).
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: 1Shadowing a field with a binding is legal but confusing. It's shown here so you recognise it, not as a style to copy.
Expected output
inside if: pattern
after if: field
inside else: field
after if: field
doubled: 42
final binding: 21Object 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;.
Break #2
Test a generic type you can't check
With Object o = List.of("a");, write if (o instanceof List<String> list).
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.
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 anifstatement whosethenbranch can't complete normally introduces the when-false set into the following statements. - ▸
Before Java 16,
equalsmethods that usedgetClass() != o.getClass()were common partly because the instanceof-then-cast form was verbose. With a pattern,instanceofequals 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
ClassCastExceptionimpossible 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 son aString) is now allowed ininstanceof, 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
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 sis the pattern andsis a pattern variable (also called a binding). Ifois a non-nullString, the test istrueandsholdsoalready typed asString. If not, the test isfalseandsis never assigned. It was previewed in Java 14 and 15 and became standard in Java 16 (JEP 394). - 2
The binding's scope follows the logic, not the braces. The compiler gives
sa scope only where the match is definitely true: inside theifblock, on the right side of&&(o instanceof String s && s.length() > 5), and after anifwhose 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 theelsebranch of a positive test,sdoesn't exist and you getcannot find symbol. - 3
**
nullnever matches.**null instanceof String sisfalse, so the binding is nevernulland you get a free null check. That is whyreturn o instanceof Money m && amount == m.amount;is a complete, null-safe start to anequalsmethod (Topic 4.8). - 4
The compiler still rejects tests that can never succeed (
IntegervsStringgivesincompatible types) and tests that can't be checked at run time because of erasure:o instanceof List<String> listfrom anObjectfails withObject cannot be safely cast to List<String>(Topic 8.6). From Java 21, a pattern that always matches, likeString son aStringexpression, is allowed; Java 16 to 20 rejected it. - 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 sto 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
Under the hood nothing new happens at run time.
javaccompileso instanceof String sto the sameinstanceof,checkcastandastorebytecode 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 inswitch, and Topic 11.3 adds record patterns that take objects apart.
Explain it without notes
What problem does pattern matching for instanceof solve, and when did it become standard?
Explain flow scoping with &&, || and an early return.
Why can the binding never be null?
Why does o instanceof List<String> l fail to compile when o is an Object?
What bytecode does o instanceof String s compile to, and is there a performance cost?
Practice
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.
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.
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
instanceofchains 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 positiveifblock is clearer. - ↔
instanceof-basedequalsis short and null-safe, but allows a subclass instance to equal a parent instance. Use it in final classes and records; usegetClass()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
equalswith a type pattern.