# Pattern Matching

Course: [Java](https://www.devtutorials.ai/learn/java) · Section: Modern Java Features
Source: https://www.devtutorials.ai/learn/java/modern-java-features/pattern-matching

> 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**.

```java runnable title="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:

```text
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:

| Feature | Java version |
|---|---|
| Pattern matching for `instanceof` | Java 16 |
| Record patterns (taking a record apart) | Java 21 |
| Pattern matching for `switch` | Java 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:

```text
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.

```java runnable title="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
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**.

```text
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.

```java runnable title="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:

```text
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:

```text
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.

```java runnable title="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:

```text
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.

```text
// 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.

```java runnable title="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:

```text
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 casting | Pattern matching |
|---|---|---|
| Steps | Check, cast, store | One step |
| Type written | Three times | Once |
| Cast written by you | Yes | No |
| Risk of casting to the wrong type | Yes, gives `ClassCastException` | No |
| Variable available | Inside the block, after the cast | Only where the match is certain |
| Works since | Java 1.0 | Java 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.

```text
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`:

```text
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.

```java runnable title="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:

```text
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:

```java runnable expect-error title="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.

```text
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.

```java runnable title="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:

```text
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 style | Pattern matching style |
|---|---|
| `if (p instanceof Cash) { Cash c = (Cash) p; ... }` | `case Cash c -> ...` |
| A long `if ... else if` chain | One `switch` with a case per type |
| `c.amount()` after a cast | `case Cash(int amount) -> ...` |
| `if (x == null)` before the switch | `case null -> ...` |
| Extra `if` checks inside a branch | `case 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.

| Symptom | Cause | Fix |
|---|---|---|
| `cannot find symbol` for a pattern variable | It was used where the match is not certain | Use it only inside the matching branch or after `&&` |
| `dominated by a preceding case label` | A general case comes before a specific one | Put specific and guarded cases first |
| `does not cover all possible input values` | The `switch` is not complete | Add `default`, or cover every type of a sealed interface |
| `NullPointerException` from a `switch` | The value was `null` and there is no `case null` | Add `case null` |
| `already defined` error on a pattern variable | Its name clashes with another variable | Choose 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
