skip to content

Pattern Matching

Modern Java's pattern matching across instanceof, switch and record deconstruction, replacing chains of casts with declarative shapes. Expect it in any interview that touches recent Java versions or sealed hierarchies.

part ofJavaoverview, primer and where to startread it →
on this pageshow

explore

questions

14

What is the instanceof pattern matching feature in Java, and how does it improve on the classic instanceof-then-cast idiom?

level: juniorimportance: must knowfreq 70%

answer

  1. Test and cast in one step
  2. Type pattern = `Type name`
  3. Pattern variable already cast
  4. Java 16, JEP 394
  5. Foundation for switch/record patterns

basics

~20 s

instanceof pattern matching lets you test a type and bind the value to a typed variable in one step: if (obj instanceof String s) gives you s already cast to String, so you skip the separate, manual cast.

solid answer

~40 s

Before Java 16, you wrote `if (obj instanceof String) { String s = (String) obj; ... }` — a redundant cast that repeats the type and can drift out of sync. The instanceof pattern (a 'type pattern') combines the test and the bind: `if (obj instanceof String s)` declares a pattern variable `s` that the compiler has already cast to String when the test succeeds. It removes the boilerplate cast, eliminates a class of bugs where the cast target diverges from the tested type, and reads more declaratively. The pattern variable is in scope wherever the test is known to be true (flow scoping). It became a standard feature in Java 16 (JEP 394) and is the foundation for record patterns and switch pattern matching.

go deeper

for a junior

Knows the syntax obj instanceof String s binds an already-cast variable, removing the manual cast.

for a middle

Explains why it is safer (no type drift between test and cast), that null doesn't match, and that scope follows the true-branch.

for a senior

Frames it within the pattern-matching family (switch, records), discusses flow scoping precisely, and notes it is compile-time sugar over the same runtime test.

for a principal

Discusses migration strategy across a large codebase, readability/standardization trade-offs, and how type patterns set up exhaustive switch over sealed hierarchies.

## The problem it solves `instanceof` is Java's operator for asking "is this object an instance of this type?" — it returns a boolean. Historically, after confirming the type you still had to *cast* the object to use it as that type: ```java Object obj = ...; if (obj instanceof String) { // 1. test the type String s = (String) obj; // 2. cast it manually System.out.println(s.length()); } ``` This has two annoyances. First, **redundancy**: you name the type `String` twice. Second, a **correctness trap**: nothing forces the cast target to match the tested type — you could test `instanceof String` but cast to `(CharSequence)`, and the compiler wouldn't complain, so the two can drift apart during refactoring. ## The feature **instanceof pattern matching** (introduced as a preview in Java 14, finalized in Java 16, JEP 394) lets the `instanceof` operator take a **type pattern** instead of just a type. A type pattern is `Type variableName`: ```java if (obj instanceof String s) { // test AND bind in one step System.out.println(s.length()); // s is already a String here } ``` Here `s` is called a **pattern variable** (also called a binding variable). When the runtime test `obj instanceof String` succeeds, the compiler *automatically* casts `obj` to `String` and assigns it to `s`. You never write the cast yourself. ## Why it is safer and cleaner - **No redundant cast** — the type is stated once. - **No drift bug** — the bound variable's type is, by construction, the tested type. - **More declarative** — the code says "if obj is a String called s" rather than "test, then cast". ## Where the variable is usable: flow scoping (preview) The pattern variable is in scope exactly where the compiler can *prove* the match succeeded. In the simple `if` body above, that's the then-branch. This is called **flow scoping** and is the deepest part of the feature (covered in detail in a separate question): the variable's reach follows the program's logical flow, not lexical braces alone. ## Relationship to the broader feature The type pattern in `instanceof` is the first and simplest **pattern**. The same pattern concept later powers **switch pattern matching** (`case String s ->`) and **record patterns** (`case Point(int x, int y) ->`). Learning instanceof patterns is the entry point to all of Java's modern pattern matching. ## Terms recap - **instanceof**: boolean operator testing runtime type. - **Type pattern**: `Type name`, the thing instanceof now accepts. - **Pattern variable / binding variable**: the new variable the pattern introduces, already cast. - **Flow scoping**: the rule deciding where that variable is in scope.

  • Does instanceof pattern matching match null?
    No. Just like classic instanceof, `null instanceof String s` is false, so the pattern variable is never bound for a null value — this is actually a convenient built-in null guard.
  • Which Java version made it a standard (non-preview) feature?
    Java 16 (JEP 394). It was previewed in Java 14 and 15.

saying these in an interview costs you the question

  • Saying you still need a manual cast inside the if body
  • Claiming it changes runtime behavior (it is the same instanceof test, just sugar plus flow scoping)
  • Thinking the pattern variable exists even when the test is false
  • Confusing it with generics or with `getClass() ==`

context

open as a page

What is switch pattern matching in Java, and how do type patterns in case labels differ from the traditional constant-based switch?

level: juniorimportance: must knowfreq 60%

basics

~20 s

Switch pattern matching lets each case test the type of the value instead of comparing it to a constant. A type pattern like case Circle c matches when the value is a Circle and binds it to c, which you can use directly in that branch.

open as a page

Explain flow scoping for instanceof pattern variables. Where is the binding variable in scope, and why does it work after && but not after ||?

level: middleimportance: must knowfreq 60%

basics

~20 s

The pattern variable is in scope only where the compiler is sure the match was true. After && the match must hold for the right side to run, so it's usable there; after || the right side runs precisely when the match failed, so it isn't.

open as a page

How does a sealed type hierarchy let a switch expression be exhaustive without a default arm, and why is omitting default sometimes preferable?

level: seniorimportance: must knowfreq 55%

basics

~20 s

A sealed type lists all its permitted subtypes, so the compiler knows the complete set of possibilities. If your switch has an arm for each one, it covers everything and you can skip default. Omitting default means adding a new subtype later causes a compile error, reminding you to handle it.

open as a page

What is a record deconstruction pattern in Java, and how does it differ from a plain type pattern?

level: juniorimportance: should knowfreq 55%

basics

~20 s

A record pattern matches a record and pulls its components into variables in one step. Instead of binding the whole record like 'Point p', you write 'Point(int x, int y)' to get x and y directly without calling p.x() and p.y().

open as a page

How does instanceof pattern matching behave with null, and what are common pitfalls when relying on it as a null check?

level: middleimportance: should knowfreq 40%

basics

~20 s

null instanceof Type t is always false, so the pattern variable is never bound for null — instanceof gives you a free null check. The pitfall: switch pattern matching changed null handling, and don't assume the variable is non-null without the test passing.

open as a page

How do nested record patterns work, and how can 'var' be used inside a record pattern?

level: middleimportance: should knowfreq 50%

basics

~20 s

If a record's component is itself a record, you can write a pattern inside a pattern to reach deep values in one line, like 'Line(Point(var x1, var y1), Point(var x2, var y2))'. Using 'var' lets the compiler infer each component's type so you don't repeat it.

open as a page

How do you handle null in a switch pattern matching, and what does case null, default mean?

level: middleimportance: should knowfreq 50%

basics

~20 s

A normal pattern case never matches null, so a null selector throws NullPointerException unless you add case null. Writing case null, default means null is handled by the same branch as everything not otherwise matched.

open as a page

What are guarded patterns (case ... when ...) in a switch, and how do they affect matching and exhaustiveness?

level: middleimportance: should knowfreq 48%

basics

~20 s

A guard is a boolean condition added to a case with the when keyword, like case Integer i when i > 0. The arm matches only if the type pattern matches AND the condition is true; otherwise checking continues with the following cases.

open as a page

Is an instanceof pattern variable effectively final? What happens if you reassign it, and why does that matter?

level: seniorimportance: should knowfreq 35%

basics

~20 s

A pattern variable behaves like a normal local variable: it isn't automatically final, so you can reassign it, but if you do it stops being effectively final. Most guidance says don't reassign it — treat it as read-only.

open as a page

How do record patterns combine with switch and sealed types, including exhaustiveness, guards, and dominance ordering?

level: seniorimportance: should knowfreq 48%

basics

~20 s

You can use record patterns as switch case labels to branch on the shape of data. If the type being switched is a sealed hierarchy and every permitted subtype is covered, the switch is exhaustive and needs no default. You can refine a case with 'when' (a guard), and you must order cases so a more specific one comes before a more general one.

open as a page

Explain case-label dominance ordering and the MatchException in pattern switches. When does each arise?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Dominance means a more general case must come after the more specific ones it would otherwise swallow; putting it first is a compile error because later arms become unreachable. MatchException is a runtime error thrown when an exhaustive-looking switch meets a value no arm covers, usually due to mismatched separate compilation.

open as a page

instanceof pattern matching makes type-checking chains easy to write. When is a chain of instanceof patterns a code smell, and when is it the right tool over polymorphism?

level: principalimportance: should knowfreq 30%

basics

~20 s

If you own the type hierarchy and the behavior belongs to the objects, prefer polymorphism (a method each subtype overrides). Long instanceof chains are a smell there. instanceof patterns are right when you can't change the types, when adding behavior would pollute them, or over sealed types with switch.

open as a page

How does type inference work for generic record patterns, and what are the rules and limits around inferring type arguments?

level: principalimportance: nice to knowfreq 28%

basics

~20 s

When a record is generic, like 'Box<T>', you can often write 'Box(var content)' and let the compiler figure out T from the value you're matching. You don't have to spell out 'Box<String>' if the surrounding type already tells the compiler what T is.

open as a page