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

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

Pro Tips

📋 Quick Reference

ConceptSyntaxNote
instanceof patterno instanceof String sBinds 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 patterncase Integer i when i > 0Adds a boolean test
Null labelcase null -> ...Avoids NPE
Null + defaultcase null, default -> ...null acts as fallback
Case orderspecific before broadFirst match wins
instanceof sinceJava 16JEP 394
switch patterns sinceJava 21JEP 441
Binding scopewhere match is trueFlow-sensitive

📚 Resources / Keep Learning

🎉 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

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.

Related lessons