The classic instanceof check is three statements doing one logical thing: ask if the object is a particular type, cast the reference to that type, and store it in a new variable. Three places to mention the type, three places to mismatch. Java 16 finalized pattern matching for `instanceof`, which folds the test, the cast, and the binding into one expression. This lesson covers the precise scoping rules of the pattern variable, how the compiler tracks flow typing across &&, ||, and !, what the compiler rejects and why, and how generics interact with the feature.
Look at the shape of the classic idiom once more. A cart needs to print a line per item, and each subtype has its own field to format.
The body of the if does three things that all say "Book":
instanceof check asks "is it a Book?"(Book) item asserts "treat it as a Book."book is declared with type Book.The information is the same in all three lines, but the compiler can't carry it from line one into lines two and three on its own. So it has to be spelled out. That's the redundancy. And because the cast is a separate statement, nothing prevents (Electronics) item from being written by mistake, with the failure surfacing at runtime.
Pattern matching collapses all three lines into one.
item instanceof Book book is called a type pattern. The Book book part declares a pattern variable named book. When the runtime check succeeds, the compiler binds book to the same reference as item, typed as Book, and makes it available inside the if body. When the check fails, book is never assigned, and the compiler enforces that it can't be read from anywhere it might be unbound.
Here's the difference side by side.
The diagram shows what disappears. The cast step (orange) is gone. The variable declaration is folded into the check. The body of the if goes straight to using the pattern variable. Two source-level operations are now one.
Pattern matching compiles to the same bytecode as a manual check-and-cast. There's no runtime overhead and no extra allocation. The benefit is at source level: fewer places to mismatch the type name, fewer places to introduce a ClassCastException.
This is the rule most worth understanding precisely. A pattern variable is in scope only where the compiler can prove the test was true. Anywhere else, it's as if the variable never existed. The technical name for this rule is flow scoping, and Java is one of the few languages that builds it into the type system.
The simplest case is the if body. Inside the true branch, the check succeeded, so the variable is in scope.
Outside the if block, the check might have failed, so the compiler refuses to allow reading book. That's flow scoping in its smallest form.
The rules get more interesting when checks are combined with &&, ||, and !. Read each operator as English and ask "under what condition does this expression's result hold?"
&&A && short-circuits. The right side only evaluates when the left side is true. That means anywhere on the right side of &&, the left side has already succeeded, and any pattern variable bound on the left is in scope.
The expression item instanceof Book book && book.getPages() > 300 only reaches the second half when the first half is true, so book is safely typed as Book when getPages() is called. The body of the if runs only when both halves are true, so book is in scope there too.
This is the pattern most often used in practice. Type check plus additional condition in one line, no separate cast, no nested if.
|||| is the opposite story. The right side runs when the left side is false. So a pattern variable bound on the left of || is not in scope on the right, because reaching the right side means the binding never happened.
The commented-out form fails because the if body runs whenever either check succeeded, and the compiler can't guarantee which pattern variable was bound. Two separate if branches handle the two cases cleanly, each with its own pattern variable in its own body.
The same rule means that inside an else block, a pattern variable from the if is not in scope, because else runs when the check failed.
! and Early ReturnThe most useful form combines a negated pattern with an early return or throw. The body of if (!(value instanceof Type t)) runs when the check failed, so t isn't in scope inside that body. But when the body always exits the method, the compiler knows that any code reached afterwards must have come from the case where the check succeeded. So t becomes in scope after the if block.
This is the validate-then-use shape. Reject the bad case at the top of the method with ! plus early return, then use the bound pattern variable in the rest of the body. Most real-world instanceof code ends up in this shape because it reads top to bottom without nesting.
The compiler's reasoning here is the same idea as definite-assignment analysis for ordinary final variables: it walks every path through the method and asks "by the time this point is reached, is book definitely bound?" If yes, the variable is in scope. If no, it isn't.
Here's the full picture of where the variable lives in each shape.
The top half shows the plain if. The bottom half shows the negated form with early return. The takeaway: the green nodes are where the pattern variable can be used, and they always sit on paths where the runtime check has been proven true.
The flow-scoping rule for && is what makes the feature feel natural in everyday code. The type check and a follow-up condition fit in one expression, with no temporary cast and no nesting.
Each branch reads as one sentence: "if it's a Book and it has more than 300 pages, it's a premium book." There's no awkward nested if for the page-count check, and no cast. Compare this to the pre-Java-16 version, which would have required nesting:
Six lines to do the work of one, and the else chain now has to be carefully nested under the right outer if. Pattern matching plus && flattens it.
The compiler doesn't only check that the pattern variable is used correctly. It also checks that the pattern is reachable at all. If the declared type of the operand and the requested pattern type have no possible subtype relationship, the check is rejected at compile time. This prevents dead code from being written.
Integer and String are both final/effectively-final types from each other's perspective: no class can be both. So the test could never succeed, and the compiler refuses to compile it. The language enforces that the operand could plausibly be the requested type before allowing the question.
When the operand's declared type is broader, like Object, the check is allowed because any reference type is a possible subtype of Object.
The compiler is happy because Object is broad enough to hold a String in principle. At runtime, this particular value happens to be an Integer, so the check returns false.
The pattern variable is bound only inside the scope where the compiler can prove the test succeeded. Using it outside that scope causes the build to fail. Here's a typical first-time mistake.
What's wrong with this code?
The compiler rejects this with an error along the lines of:
The pattern variable code was bound only inside the if body. After the closing brace, the compiler can't prove the check succeeded (it might have been false), so code is out of scope.
Fix (option 1): Move the use inside the if body.
Fix (option 2): Flip to the negation pattern with an early return, which puts code in scope after the if.
The negation form is usually the cleaner fix when the rest of the method only makes sense for the matching type.
A natural question: can the pattern type be a parameterized generic, like List<String>? Mostly no, for the same reason ordinary instanceof doesn't accept generic types: at runtime, the JVM only knows the raw class. Generic information is erased after compilation.
These two forms are accepted:
The wildcard form List<?> matches the runtime fact: value is some List, with no information about the element type. The raw form List works for the same reason, but mixing raw types with surrounding code triggers unchecked warnings, so prefer List<?> when the elements don't need to be used.
This is what the compiler refuses:
The reason is that erasure makes List<String> and List<Integer> indistinguishable at runtime. A check that says "is this a List<String>?" has no information to look at. Older Java versions just rejected this outright; newer versions allow it only in narrow situations where the cast is provably safe given the operand's declared type. For day-to-day code, treat parameterized type patterns as not supported: check against List<?> or use a wildcard upper bound (like List<? extends Number>) for more specificity.
To inspect the element type, iterate and check element by element:
The outer pattern checks the raw shape (it's some List). The inner stream pattern checks each element. There's no shortcut at the language level; the JVM simply doesn't keep the element type around to ask.
Pattern matching shows the biggest improvement when replacing a chain of nested casts. A typical "before" from an order pricing function: it dispatches by item type and reads subtype-specific fields to compute a per-item adjustment.
Each branch has the same shape: check, cast, then an inner condition on a subtype field. Nine lines per type. With pattern matching plus &&, the inner condition rides along on the type check.
Each pricing rule is now one line. The intent reads top to bottom: "long books get a 10% discount; long-warranty electronics get a 5% surcharge; XL apparel gets a 10% surcharge; everything else uses the listed price." A new reader can scan the rules without tracking which cast belongs to which inner block.
The same design advice still applies: with four or more branches dispatching purely on subtype, prefer overriding a method on the parent class. Pattern matching makes the chain less painful to write, but it doesn't change the design pressure for polymorphism when the behavior naturally belongs to the type.
9 quizzes