Java 21 Pattern Matching for switch

July 30, 20265 min readUpdated 8/20/2026

Java 21 lets a switch match on types, not just constants. Combined with sealed types, it turns a chain of instanceof checks into something the compiler can prove is complete.

The chain it replaces

class Demo {
    String oldWay(Object value) {
        if (value instanceof Integer i) return "int: " + i;
        if (value instanceof String s) return "text: " + s.length();
        if (value instanceof List<?> l) return "list of " + l.size();
        return "unknown";
    }

    String newWay(Object value) {
        return switch (value) {
            case Integer i -> "int: " + i;
            case String s -> "text: " + s.length();
            case List<?> l -> "list of " + l.size();
            default -> "unknown";
        };
    }
}

Each case tests a type and binds a variable of that type, the same as instanceof pattern matching. The gain over the if chain is that it is an expression — one return, and the compiler checks that every path produces a value.

Guards with when

class Demo {
    String classify(Object value) {
        return switch (value) {
            case Integer i when i < 0 -> "negative";
            case Integer i when i == 0 -> "zero";
            case Integer i -> "positive: " + i;
            case String s when s.isBlank() -> "blank text";
            case String s -> "text: " + s;
            default -> "other";
        };
    }
}

Order matters, and the compiler enforces it. A guarded case must come before the unguarded case for the same type — put case Integer i first and the guarded ones below are unreachable, which is a compile error rather than a silent bug.

when is a contextual keyword, so any existing variable called when keeps working.

case null

class Demo {
    String describe(Object value) {
        return switch (value) {
            case null -> "nothing";              // now legal
            case String s -> "text: " + s;
            default -> "something";
        };
    }

    String stillThrows(Object value) {
        return switch (value) {
            case String s -> "text";
            default -> "other";                  // does NOT catch null
        };
    }
}

This fixes a genuine wart. Before Java 21, switch on a null reference threw NullPointerException — including when there was a default, which surprised everyone. You can now handle null explicitly.

The old behaviour is preserved when you do not: a switch with no case null still throws. That is deliberate backwards compatibility, and it means adding case null is opt-in. case null, default -> ... combines the two when you want them to behave the same.

Exhaustiveness with sealed types

This is where the feature earns its place:

sealed interface Payment permits Card, BankTransfer, StoreCredit { }
record Card(String last4) implements Payment { }
record BankTransfer(String iban) implements Payment { }
record StoreCredit(double amount) implements Payment { }

class Processor {
    String describe(Payment payment) {
        return switch (payment) {                // no default needed
            case Card c -> "card ending " + c.last4();
            case BankTransfer b -> "transfer from " + b.iban();
            case StoreCredit s -> "credit of " + s.amount();
        };
    }
}

Because Payment is sealed, the compiler knows the complete list of subtypes and can verify you covered them. Add a fourth payment kind and every switch like this stops compiling — you get the list of places to update instead of a silent default swallowing the new case.

Do not add a default to an exhaustive switch. It compiles, and it throws away the guarantee.

Record patterns

Types and their contents can be matched together. Record patterns has the detail; the shape is worth seeing here because it is where the feature becomes genuinely expressive:

sealed interface Shape permits Circle, Rectangle { }
record Circle(double radius) implements Shape { }
record Rectangle(double width, double height) implements Shape { }

class Geometry {
    String describe(Shape shape) {
        return switch (shape) {
            case Circle(double r) when r > 100 -> "big circle";
            case Circle(double r) -> "circle of radius " + r;
            case Rectangle(double w, double h) when w == h -> "square of " + w;
            case Rectangle(double w, double h) -> w + " by " + h;
        };
    }
}

Dominance, and the error you will meet

The compiler rejects any case that can never be reached because an earlier one already covers it. It calls this dominance, and the message names it:

class Demo {
    String broken(Object value) {
        return switch (value) {
            case Object o -> "anything";
            // case String s -> "text";     // error: this case label is dominated
        };
        // Note: no `default` here either. `case Object o` already matches
        // everything, and a switch cannot have both an unconditional pattern
        // and a default label — that is a second, separate compile error.
    }

    String fixed(Object value) {
        return switch (value) {
            case String s -> "text";        // most specific first
            case Object o -> "anything";
        };
    }
}

The rule is the same one that governs catch blocks: specific before general, or the general one swallows everything. It applies to guards too — an unguarded case dominates every guarded case for the same type, which is why the guarded ones must come first.

This is a good error to get. The equivalent mistake in an if/else if chain compiles silently and the branch simply never runs.

What it does not replace

The same caution as the instanceof post: if you own the types and the behaviour belongs to them, a method on the interface beats a switch. Adding a shape then requires no edit at all, whereas the switch above needs one — even though the compiler will remind you.

The switch is the better answer when the behaviour belongs to the caller rather than the type (formatting for one particular screen), when the hierarchy is not yours to change, or when you need the exhaustiveness check as documentation of a decision table.

There is also a legitimate objection that type-switching is object-orientation turned inside out. It is — and sealed types are what make it safe, because the set is closed and checked. Type-switching over an open hierarchy still deserves the suspicion it always did.

Next

Record Patterns is next — destructuring a record into its components as part of the match.