Pattern Matching (instanceof & switch)
Reviewed & published by Brayan K
Pattern matching lets instanceof and switch test a value's type and bind it to a ready-to-use, correctly typed variable in a single step — no separate cast required.
Part of the free Java course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
Before You Start
You should know inheritance and casting and the new switch expressions. instanceof patterns need Java 16+; switch patterns need Java 21+.
What You'll Learn
- Binding a variable with instanceof — no cast
- Type patterns inside a switch
- Guarded patterns with the when keyword
- Handling null safely with case null
- Why case order matters (first match wins)
- Replacing long instanceof chains cleanly
The Big Idea — Sort the Mail as You Open It
💡 Analogy: The old way was like a mailroom worker who first checks "is this a parcel?" and then, in a separate step, picks up the parcel to weigh it (if (o instanceof Box) { Box b = (Box) o; ... }). Pattern matching is the worker who recognises the parcel and already has it in hand: if (o instanceof Box b) — one motion. A pattern switch is the sorting table where each chute is labelled by type, and whatever lands there arrives already identified.
The win is fewer casts, fewer chances to cast to the wrong type, and code that reads as "when it's a String named s, do this".
1️⃣ Pattern Matching for instanceof
Write the type test and a variable name together: o instanceof String s. If o is a String, the binding variable s is in scope and already typed — call s.length() with no cast. Compare the before/after:
// before
if (o instanceof String) {
String s = (String) o; // manual cast
use(s);
}
// after
if (o instanceof String s) { // bind in the test
use(s);
}public class Main {
static String describe(Object obj) {
// Pattern matching for instanceof: test AND bind in one step.
if (obj instanceof String s) {
// 's' is already a String here — no cast needed.
return "String of length " + s.length();
} else if (obj instanceof Integer i) {
return "Integer doubled = " + (i * 2);
}
return "unknown";
}
public static void main(String[] args) {
System.out.println(describe("hello"));
System.out.println(describe(21));
System.out.println(describe(3.14));
}
}2️⃣ Type Patterns in switch
A pattern switch dispatches on the type of its selector. Each case Type name -> ... matches when the value is that type and binds name. This collapses a tower of if/else instanceof checks into one tidy switch. Add an explicit case null so a null selector is handled instead of throwing.
public class Main {
static String classify(Object obj) {
// Type patterns inside a switch — the binding variable comes after
// the type. 'null' has its own label so it never throws.
return switch (obj) {
case null -> "it was null";
case Integer i -> "int " + i;
case String s -> "string of " + s.length() + " chars";
case int[] arr -> "int array of " + arr.length;
default -> "something else: " + obj.getClass().getSimpleName();
};
}
public static void main(String[] args) {
System.out.println(classify(42));
System.out.println(classify("hi there"));
System.out.println(classify(new int[]{1, 2, 3}));
System.out.println(classify(null));
System.out.println(classify(2.5));
}
}3️⃣ Guarded Patterns with when
A guard adds a boolean condition to a pattern using when: case String s when s.isEmpty() -> .... The arm matches only if the type matches and the guard is true. Because arms are tried top to bottom, list the most specific guarded patterns first and the plain pattern last as a catch-all for that type.
public class Main {
static String size(Object obj) {
// Guarded patterns: a 'when' clause adds a boolean test to a pattern.
return switch (obj) {
case String s when s.isEmpty() -> "empty string";
case String s when s.length() < 5 -> "short string: " + s;
case String s -> "long string: " + s;
case Integer i when i < 0 -> "negative int";
case Integer i -> "non-negative int: " + i;
default -> "other";
};
}
public static void main(String[] args) {
System.out.println(size(""));
System.out.println(size("hey"));
System.out.println(size("hello world"));
System.out.println(size(-7));
System.out.println(size(100));
}
}public class Main {
public static void main(String[] args) {
Object[] values = {" trim me ", 256, "x", -1, 3.0};
for (Object v : values) {
// Using the bound variable in a guard AND in the result.
String msg = switch (v) {
case String s when s.isBlank() -> "blank!";
case String s -> "trimmed='" + s.trim() + "'";
case Integer i when i > 100 -> "big int " + i;
case Integer i -> "small int " + i;
default -> "?";
};
System.out.println(msg);
}
}
}🧠 Quick Recall
Predict the result before opening each answer.
if (o instanceof String s) {}
System.out.println(s.length());Answer: No. Outside the if, the pattern isn't provably true, so s is out of scope: error: cannot find symbol s. Use s only where the match holds.
switch (n) {
case Integer i -> "any int";
case Integer i when i < 10 -> "small";
}Answer: This won't compile. The plain case Integer i comes first and matches every Integer, so the guarded arm below is unreachable — a compile error. Swap their order.
String s = switch (obj) {
case Integer i -> "int";
default -> "other";
};Answer: If obj is null, it throws NullPointerException — a pattern switch only handles null if you add case null. Add case null -> "none"; to be safe.
🎯 Your Turn — instanceof with a binding
Combine a pattern with && to only match long strings, and handle integers too.
public class Main {
static String label(Object o) {
// YOUR TURN: use pattern matching for instanceof.
// - if o is a String LONGER than 3 chars -> "long word: " + uppercase
// - else if o is an Integer -> "number: " + (value + 1)
// - otherwise -> "other"
// Hint: you can combine a pattern with && : o instanceof String s && s.length() > 3
//
// Expected:
// long word: HELLO
// other
// number: 10
// other
return "TODO";
}
public static void main(String[] args) {
System.out.println(label("hello"));
System.out.println(label("hi"));
System.out.println(label(9));
System.out.println(label(2.0));
}
}🧩 Mini-Challenge — A grading pattern switch
Use guarded patterns and a case null label — and get the order right.
public class Main {
static String grade(Object o) {
// MINI-CHALLENGE: a pattern switch that grades a score.
// - case null -> "no score"
// - Integer >= 90 -> "A"
// - Integer >= 60 -> "Pass (score)"
// - any other Integer -> "Fail (score)"
// - anything else -> "invalid"
// Use guarded patterns (when) and remember: order matters.
//
// Expected:
// A
// Pass (72)
// Fail (40)
// no score
// invalid
return "TODO";
}
public static void main(String[] args) {
System.out.println(grade(95));
System.out.println(grade(72));
System.out.println(grade(40));
System.out.println(grade(null));
System.out.println(grade("x"));
}
}Common Errors
- ❌ Using the binding variable out of scope. error: cannot find symbol. Fix: use it only where the pattern is provably true.
- ❌ Putting a broad pattern before a guarded one. The guarded arm becomes unreachable. Fix: specific/guarded cases first.
- ❌ Forgetting case null in a pattern switch. A null selector throws. Fix: add case null -> ....
- ❌ Still writing a manual cast after the pattern. Redundant. Fix: use the binding variable directly.
- ❌ Non-exhaustive pattern switch expression. Like any switch expression, it must cover all inputs. Fix: add a default (or cover the whole hierarchy).
- ❌ Reusing a binding name that shadows a field/variable. Confusing. Fix: pick a fresh, descriptive name.
Pro Tips
- 💡 Use a negated pattern to fail fast: if (!(o instanceof String s)) return; — after it, s is in scope for the rest of the method.
- 💡 Pattern switch shines with sealed types (next-but-one lesson): the compiler proves your switch is exhaustive without a default.
- 💡 Keep guards cheap and side-effect-free. A when clause may be evaluated as the switch searches for a match.
- 💡 Combine case null, default when null should behave like the fallback.
📋 Quick Reference
| Concept | Syntax | Note |
|---|---|---|
| instanceof pattern | o instanceof String s | Binds s, no cast |
| Pattern + && | o instanceof String s && ... | s usable in rest of && |
| Negated pattern | !(o instanceof String s) | s in scope after return |
| Type pattern (switch) | case String s -> ... | Matches by type |
| Guarded pattern | case Integer i when i > 0 | Adds a boolean test |
| Null label | case null -> ... | Avoids NPE |
| Null + default | case null, default -> ... | null acts as fallback |
| Case order | specific before broad | First match wins |
| instanceof since | Java 16 | JEP 394 |
| switch patterns since | Java 21 | JEP 441 |
| Binding scope | where match is true | Flow-sensitive |
📚 Resources / Keep Learning
- 📘 Oracle: Pattern Matching — the official language guide.
- 📗 dev.java: Pattern Matching — a guided tutorial series.
- 📙 JEP 441: Pattern Matching for switch — the finalizing proposal.
🎉 Lesson Complete!
You can now bind a typed variable straight from instanceof, dispatch on type with a pattern switch, refine matches with when guards, handle null with case null, and order cases so the first match is the right one.
Practice quiz
What does if (o instanceof String s) give you?
- A boolean only
- A test plus a String variable s already cast
- A new String object
- Nothing useful
Answer: A test plus a String variable s already cast. Pattern matching for instanceof tests the type AND binds s as a String in one step — no explicit cast.
Inside the body where the pattern matched, do you still need to cast?
- Yes, always
- No — the binding variable is already the matched type
- Only for primitives
- Only in switch
Answer: No — the binding variable is already the matched type. The binding variable is automatically the narrowed type, so the old (String) o cast disappears.
In a switch, what is case String s -> ... called?
- A constant label
- A type pattern
- A guard
- A default
Answer: A type pattern. Matching by type and binding a variable in switch is a type pattern.
What keyword adds a boolean condition to a switch pattern?
- if
- where
- when
- guard
Answer: when. A guarded pattern uses when: case Integer i when i > 100 -> ...
How do you handle null in a pattern switch so it doesn't throw?
- It is handled by default automatically
- Add an explicit case null -> ... label
- Wrap in try/catch
- Use Optional
Answer: Add an explicit case null -> ... label. A traditional switch throws on null; a pattern switch lets you add case null to handle it safely.
Does a traditional switch (no patterns) throw on a null selector?
- No
- Yes, NullPointerException
- It returns null
- It skips to default
Answer: Yes, NullPointerException. A classic switch throws NullPointerException on null unless you guard it beforehand; pattern switch can add case null.
Where can you use the binding variable from a guarded pattern?
- Only in the guard
- Only in the body
- In both the when guard and the arm body
- Nowhere
Answer: In both the when guard and the arm body. The pattern variable is in scope for the guard and the arm body.
In which Java version did pattern matching for instanceof become standard?
- Java 8
- Java 14
- Java 16
- Java 21
Answer: Java 16. instanceof pattern matching finalized in Java 16 (JEP 394).
When did pattern matching for switch become a standard feature?
- Java 16
- Java 17
- Java 21
- Java 8
Answer: Java 21. Pattern matching for switch was finalized in Java 21 (JEP 441).
Why must a more specific guarded case come BEFORE a broader one?
- Style only
- Cases are tried top to bottom; the first match wins
- The compiler reorders them
- It does not matter
Answer: Cases are tried top to bottom; the first match wins. A pattern switch checks arms in order, so put specific/guarded patterns before the general fallback.
Continue this course
- Previous: Switch Expressions
- Next: equals() & hashCode() — Why they must agree, the contract, and Objects.equals / Objects.hash
- Quick reference: Java cheat sheet
Frequently asked questions
What is pattern matching for instanceof?
It is a shorthand that combines a type test with a cast and a variable binding. Instead of writing if (o instanceof String) { String s = (String) o; ... } you write if (o instanceof String s) { ... } and s is already a String inside the block. The 's' is called a binding (or pattern) variable. This removes the repetitive, error-prone cast and was finalized in Java 16.
How is pattern matching in switch different from a normal switch?
A classic switch matches a value against constants (case 1:, case "GET":). A pattern switch matches against types: case Integer i -> ... runs when the selector is an Integer and binds it to i. This lets a single switch dispatch on the runtime type of an Object and immediately work with the narrowed value — something that previously required a chain of instanceof checks. It became standard in Java 21.
What is a guarded pattern and the when keyword?
A guard refines a type pattern with an extra boolean test using when: case Integer i when i > 100 -> "big". The arm only matches if the value is an Integer AND the guard is true. The binding variable is in scope inside the guard, so you can test the very value you just bound. Because arms are checked top to bottom, you place the more specific guarded patterns before the broader, unguarded ones.
How does a pattern switch handle null?
A traditional switch throws a NullPointerException if the selector is null. A pattern switch gives you control: add an explicit case null -> ... label and null is handled safely on that arm. You can even combine it with the default arm as case null, default -> .... Without a case null label, a pattern switch still throws on null, so add the label whenever null is possible.
Why does the order of cases matter?
A pattern switch evaluates its arms from top to bottom and takes the first one that matches. So a more specific or guarded pattern must come before a broader one that would also match. For example, case Integer i when i < 0 must precede case Integer i, otherwise the general Integer arm would catch every Integer first and the guarded arm would be unreachable — which the compiler actually flags as an error.