Pattern Matching in Java

By the Syncfusion DevTutorials team · Last updated

Check a value's type and use it in one step with pattern matching for instanceof and switch, including guards, null cases and record patterns.

Think about sorting items at a post office.

A worker picks up each item and asks one question: “What kind of item is this?” A letter goes into the letter rack. A parcel goes on the parcel shelf. A heavy parcel goes on the floor.

The worker does two things at once: they check what the item is, and they handle it in the right way.

Pattern matching gives Java the same idea. In one step, it checks what type a value is and gives you a variable of that type, ready to use.

In simple words: pattern matching tests the shape of a value and, if it matches, gives you the value in a ready-to-use form.

What You Will Learn

In this lesson, you will learn:

  1. What pattern matching is and why Java added it.
  2. How pattern matching works with instanceof.
  3. What type patterns are.
  4. How pattern variables work and where you can use them.
  5. How pattern matching compares with traditional casting.
  6. How to use patterns inside switch, including guards and records.

Start With a Working Example

Press Run.

One switch handles three different shapes

public class Main {
    public static void main(String[] args) {
        Object[] shapes = {
            new Circle(3),
            new Square(4),
            new Rectangle(2, 5)
        };

        for (Object s : shapes) {
            System.out.println(describe(s));
        }
    }

    static String describe(Object shape) {
        return switch (shape) {
            case Circle c -> "Circle with area " + Math.round(3.14 * c.radius() * c.radius());
            case Square s -> "Square with area " + (s.side() * s.side());
            case Rectangle r -> "Rectangle with area " + (r.width() * r.height());
            default -> "Unknown shape";
        };
    }
}

record Circle(int radius) { }
record Square(int side) { }
record Rectangle(int width, int height) { }

Expected output:

Circle with area 28
Square with area 16
Rectangle with area 10

case Circle c checks whether shape is a Circle. If it is, Java creates a variable c of type Circle, ready to use. There is no separate check and no cast.

Try This

Add a fourth item to the array: "a triangle". It is a String, so it matches none of the cases, and the default line prints Unknown shape.

What is Pattern Matching?

Pattern matching means testing whether a value has a certain shape, such as a certain type, and, if it does, pulling the parts you need out of it in the same step.

A pattern is the thing you test against. In Java, the most common pattern is a type pattern, like String s, which means "is it a String? If yes, call it s".

Pattern matching was added to Java in stages:

FeatureJava version
Pattern matching for instanceofJava 16
Record patterns (taking a record apart)Java 21
Pattern matching for switchJava 21

The goal is the same each time: less code, fewer casts, and fewer mistakes.

Pattern Matching for instanceof

You have used instanceof to check an object's type. Before Java 16, it only gave you true or false. You then had to cast the object yourself.

With pattern matching, you add a variable name after the type:

if (obj instanceof String s) {
    // s is a String here
}

If obj is a String, the check is true, and s is a String variable holding the same object. If not, the check is false, and s is never created.

Check and get a typed variable in one step

public class Main {
    public static void main(String[] args) {
        Object[] items = {"Chennai", 42, 3.5, true};

        for (Object item : items) {
            if (item instanceof String text) {
                System.out.println("Text with " + text.length() + " letters: " + text);
            } else if (item instanceof Integer number) {
                System.out.println("Whole number, doubled: " + (number * 2));
            } else if (item instanceof Double decimal) {
                System.out.println("Decimal, halved: " + (decimal / 2));
            } else {
                System.out.println("Something else: " + item);
            }
        }
    }
}

Expected output:

Text with 7 letters: Chennai
Whole number, doubled: 84
Decimal, halved: 1.75
Something else: true

Inside each branch, the variable already has the right type. text.length() works because text is a String. number * 2 works because number is an Integer.

Type Patterns

A type pattern has two parts: a type and a variable name.

String text
│      │
type   pattern variable

It matches when the value is that type, or a child of that type. For example, the pattern Number n matches an Integer, a Double and a Long, because they all extend Number.

A type pattern never matches null. null instanceof String s is false, so you never get a null pattern variable.

A type pattern also matches child types

public class Main {
    public static void main(String[] args) {
        Object[] values = {12, 4.5, 9_000_000_000L, "12", null};

        for (Object v : values) {
            if (v instanceof Number n) {
                System.out.println(v + " is a Number, as a double: " + n.doubleValue());
            } else {
                System.out.println(v + " is not a Number");
            }
        }
    }
}

Expected output:

12 is a Number, as a double: 12.0
4.5 is a Number, as a double: 4.5
9000000000 is a Number, as a double: 9.0E9
12 is not a Number
null is not a Number

Integer, Double and Long all matched Number n. The text "12" did not, because a String is not a Number, even if it contains digits.

null did not match either. That makes pattern matching safe: a pattern variable is never null.

9.0E9 is how Java prints large decimals: 9.0 × 10⁹.

Pattern Variables

A pattern variable is the variable a pattern creates, such as text in item instanceof String text.

Pattern variables follow special rules about where they exist. A pattern variable only exists where Java is sure the pattern matched. This is called flow scoping.

Inside the if Block

The most common case: the variable exists inside the if block.

On the Right of &&

With &&, the right side only runs if the left side was true. So the pattern variable can be used straight away on the right:

if (obj instanceof String s && s.length() > 5) { ... }

This does not work with ||. With ||, the right side runs when the left side is false, so the pattern did not match.

After a Negated Check That Exits

If you check !(obj instanceof String s) and leave the method when it is true, the variable exists after the if, because the only way to get there is if the pattern matched.

Where a pattern variable can be used

public class Main {
    public static void main(String[] args) {
        check("Bengaluru");
        check("Goa");
        check(2026);
    }

    static void check(Object value) {
        if (value instanceof String city && city.length() > 5) {
            System.out.println(city + " is a long city name");
        }

        if (!(value instanceof String name)) {
            System.out.println(value + " is not text, stopping");
            return;
        }

        System.out.println(name + " has " + name.length() + " letters");
    }
}

Expected output:

Bengaluru is a long city name
Bengaluru has 9 letters
Goa has 3 letters
2026 is not text, stopping

In the first if, city is used on the right of &&.

In the second if, the method returns when value is not a String. So after that if, Java knows the pattern matched, and name can be used on the last line.

A pattern variable cannot have the same name as another variable already in scope. If the method already has a variable called s, write obj instanceof String text instead of obj instanceof String s.

Pattern Matching vs Traditional Casting

Before pattern matching, you had to write three steps:

  1. Check the type with instanceof.
  2. Cast the object to that type.
  3. Store the result in a new variable.
// Traditional
if (obj instanceof String) {
    String s = (String) obj;
    System.out.println(s.length());
}

// Pattern matching
if (obj instanceof String s) {
    System.out.println(s.length());
}

The traditional way repeats the type name three times. It also allows a nasty mistake: checking for one type and casting to another.

The old way and the new way give the same result

public class Main {
    public static void main(String[] args) {
        Object payment = new UpiPayment("asha@bank", 750);

        if (payment instanceof UpiPayment) {
            UpiPayment upi = (UpiPayment) payment;
            System.out.println("Traditional: " + upi.id() + " paid " + upi.amount());
        }

        if (payment instanceof UpiPayment upi) {
            System.out.println("Pattern:     " + upi.id() + " paid " + upi.amount());
        }
    }
}

record UpiPayment(String id, int amount) { }

Expected output:

Traditional: asha@bank paid 750
Pattern:     asha@bank paid 750

Both print the same line. The pattern version is one line shorter, and the type name appears once instead of three times.

Traditional castingPattern matching
StepsCheck, cast, storeOne step
Type writtenThree timesOnce
Cast written by youYesNo
Risk of casting to the wrong typeYes, gives ClassCastExceptionNo
Variable availableInside the block, after the castOnly where the match is certain
Works sinceJava 1.0Java 16

Easy Way to Remember: traditional casting is check, cast, store. Pattern matching is check and store, and the cast is done for you.

Pattern Matching for switch

Since Java 21, switch can match patterns, not just fixed values like numbers and strings. Each case can be a type pattern.

switch (obj) {
    case Integer i -> ...
    case String s  -> ...
    default        -> ...
}

Java checks the cases from top to bottom and runs the first one that matches. This replaces long chains of if ... else if ... instanceof.

Guards With when

A guard adds an extra condition to a case. You write it with when:

case Integer i when i > 100 -> "big number"

The case only matches if the type matches and the condition is true. Put more specific cases, like guarded ones, above general ones.

Handling null

A normal switch throws a NullPointerException when the value is null. With pattern matching, you can add case null to handle it safely.

Type patterns, guards and null in one switch

public class Main {
    public static void main(String[] args) {
        Object[] inputs = {250, 12, -5, "hello", "", 9.99, null};

        for (Object in : inputs) {
            System.out.println(in + " -> " + classify(in));
        }
    }

    static String classify(Object value) {
        return switch (value) {
            case null -> "no value";
            case Integer i when i > 100 -> "a big number";
            case Integer i when i < 0 -> "a negative number";
            case Integer i -> "a small number";
            case String s when s.isEmpty() -> "empty text";
            case String s -> "text in capitals: " + s.toUpperCase();
            default -> "some other value";
        };
    }
}

Expected output:

250 -> a big number
12 -> a small number
-5 -> a negative number
hello -> text in capitals: HELLO
 -> empty text
9.99 -> some other value
null -> no value

250 matched the first Integer guard. 12 passed neither guard, so it fell to the plain case Integer i.

9.99 is a Double, which no case mentions, so default handled it.

case null caught the null safely, instead of throwing an exception.

Pattern matching for switch and record patterns need Java 21 or newer. On an older Java version, use if ... instanceof patterns, which work from Java 16.

Order Matters

A case that matches everything a later case would match makes the later case unreachable. Java refuses to compile it:

A general case above a specific one is an error

public class Main {
    public static void main(String[] args) {
        Object value = 500;
        String result = switch (value) {
            case Integer i -> "any number";
            case Integer i when i > 100 -> "big number";
            default -> "other";
        };
        System.out.println(result);
    }
}

The compiler reports this case label is dominated by a preceding case label. Every Integer is caught by the first case, so the second can never run. Swap the two cases to fix it.

Record Patterns

A record pattern takes a record apart in the case itself. You list the record's components in brackets, and each one becomes a variable.

case Point(int x, int y) -> "x is " + x + ", y is " + y

You no longer need to call p.x() and p.y().

Sealed Types and switch

With a sealed interface, Java knows every possible type. If your switch covers them all, you do not need a default case. If you later add a new type to the sealed interface, every switch that misses it stops compiling, which reminds you to handle it.

Record patterns over a sealed type, with no default

public class Main {
    public static void main(String[] args) {
        Payment[] payments = {
            new Cash(500),
            new Card("4321", 1200),
            new Upi("ravi@bank", 250)
        };

        int total = 0;
        for (Payment p : payments) {
            System.out.println(describe(p));
            total += amount(p);
        }
        System.out.println("Total collected: " + total);
    }

    static String describe(Payment p) {
        return switch (p) {
            case Cash(int amount) -> "Cash: " + amount;
            case Card(String last4, int amount) -> "Card ending " + last4 + ": " + amount;
            case Upi(String id, int amount) -> "UPI from " + id + ": " + amount;
        };
    }

    static int amount(Payment p) {
        return switch (p) {
            case Cash c -> c.amount();
            case Card c -> c.amount();
            case Upi u -> u.amount();
        };
    }
}

sealed interface Payment permits Cash, Card, Upi { }
record Cash(int amount) implements Payment { }
record Card(String last4, int amount) implements Payment { }
record Upi(String id, int amount) implements Payment { }

Expected output:

Cash: 500
Card ending 4321: 1200
UPI from ravi@bank: 250
Total collected: 1950

case Card(String last4, int amount) checks that p is a Card, then pulls out its two components as variables.

Neither switch has a default. Java knows Payment can only be Cash, Card or Upi, and all three are covered.

Old stylePattern matching style
if (p instanceof Cash) { Cash c = (Cash) p; ... }case Cash c -> ...
A long if ... else if chainOne switch with a case per type
c.amount() after a castcase Cash(int amount) -> ...
if (x == null) before the switchcase null -> ...
Extra if checks inside a branchcase Integer i when i > 100 -> ...

Common mistakes

The following mistakes are common when using pattern matching. Each row shows what you see, why it happens and how to fix it.

SymptomCauseFix
cannot find symbol for a pattern variableIt was used where the match is not certainUse it only inside the matching branch or after &&
dominated by a preceding case labelA general case comes before a specific onePut specific and guarded cases first
does not cover all possible input valuesThe switch is not completeAdd default, or cover every type of a sealed interface
NullPointerException from a switchThe value was null and there is no case nullAdd case null
already defined error on a pattern variableIts name clashes with another variableChoose a different name

What to Remember

  • Pattern matching checks a value's shape and, if it matches, gives you the parts in a ready-to-use form.
  • obj instanceof String s checks the type and creates a typed variable, with no cast.
  • A type pattern also matches child types, and never matches null.
  • A pattern variable exists only where the match is certain: inside the if, after &&, or after a negated check that exits.
  • Pattern matching replaces check, cast and store with one safer step.
  • switch can match type patterns, add guards with when, handle case null, and take records apart with record patterns.
  • Put specific cases first. A switch over a sealed type can skip default when every type is covered.

Next: Cheat Sheet