Command Palette

Search for a command to run...

PHASE 11Intermediate Java 21+ ~36 min· topic 2 of 6

Topic 11.2

Pattern Matching for switch

In one line

From Java 21, switch can test the type and shape of any object, not only constants: case Circle c -> ..., with when guards, case null, and a compiler that checks every possible type is handled. With sealed types it replaces long if/instanceof chains with one exhaustive, checked expression.

Think of it like this

A post-office sorting machine. Letters go to one chute, parcels to another, and parcels heavier than 10 kg go to a special chute first. The machine was built knowing every kind of item the post office accepts, so nothing can fall on the floor. If the post office starts accepting a new kind of item, the machine must be updated before it runs. A pattern switch is that sorting machine for objects.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Selector
The value inside switch (...) that the cases are tested against.
Case label
What follows case: a constant like 3, a pattern like String s, or null.
Guard
A when condition after a pattern, like case Integer i when i > 0. The case matches only if the condition is also true.
Dominance
When an earlier case always matches everything a later case would match, so the later case could never run. The compiler reports it as an error.
Exhaustive
Covering every possible value. An exhaustive switch can never reach the end without a matching case.
Sealed type
A class or interface that lists exactly which classes may extend or implement it (sealed ... permits A, B), so the compiler knows all its subtypes.
MatchException
An exception (Java 21) thrown when an exhaustive switch meets a value none of its cases match, which can only happen if classes changed after compilation.
yield
The keyword that gives a value back from a block { ... } inside a switch expression.

Step by step

01From an if-chain to a switch

Before Java 21, code that behaves differently for different types was a chain of if (o instanceof ...) else if .... Each branch is fine on its own, but nothing checks that the chain is complete, and the order is easy to get wrong.

A pattern switch says the same thing in one expression, each case a type pattern. Because it is an expression, it must produce a value for every input, and the compiler checks that.

Main.javawhole filejava
// before: an if-chain nobody checks for completeness
static String describeOld(Object o) {
    if (o instanceof Integer i) return "a number " + i;
    else if (o instanceof String s) return "a string of length " + s.length();
    else return "something else";
}

// Java 21: one switch expression
static String describe(Object o) {
    return switch (o) {
        case Integer i -> "a number " + i;
        case String s -> "a string of length " + s.length();
        default -> "something else";
    };
}

02Guards with when

A guard refines a pattern: case Integer i when i < 0. The pattern is checked first; only if it matches is the guard evaluated, and it can use the binding i. If the guard is false, matching continues with the next case.

So the usual layout is: guarded cases for a type first, then the unguarded case for the same type as the catch-all for that type.

Main.javawhole filejava
return switch (o) {
    case Integer i when i < 0 -> "a negative number " + i;   // checked first
    case Integer i -> "a number " + i;                       // every other Integer
    case String s when s.isBlank() -> "a blank string";
    case String s -> "a string of length " + s.length();
    default -> "something else";
};
Guards with whendiagram
Rendering diagram…

03Dominance: the compiler checks your order

Because cases are tried in order, a general case placed before a specific one would hide it forever. Every String is a CharSequence, so in the switch below case String s could never run. Java treats that as a mistake, not a warning.

The same applies to a guarded case placed after the unguarded case for the same type, and to a constant case (like case 0) placed after case Integer i.

terminal
$ javac Main.java
── expected output ──
Main.java:5: error: this case label is dominated by a preceding case label
case String s -> "string";
^
1 error

04null gets its own case

Historically switch on a null throws NullPointerException, and that is still the default. A pattern switch may add case null -> ... to handle it explicitly, or combine it with the default as case null, default -> ....

So with pattern switches, the rule is: **no case null, then null throws**. That keeps old code's behaviour and makes null handling visible where you want it.

Main.javawhole filejava
static String kind(Object o) {
    return switch (o) {
        case String s -> "text";
        case null, default -> "not text (or null)";
    };
}

05Exhaustive over a sealed type: no default needed

When the selector is a sealed interface, the compiler reads its permits list. A switch with a case for each permitted subtype is exhaustive, so it needs no default. Leave one out and you get the switch expression does not cover all possible input values.

This is the real payoff. With a default, adding a new subtype compiles silently and the new type falls into the default branch. Without one, every switch that needs to handle the new type becomes a compile error that points you to it.

Main.javawhole filejava
sealed interface Shape permits Circle, Square, Rect { }
record Circle(double r) implements Shape { }
record Square(double side) implements Shape { }
record Rect(double w, double h) implements Shape { }

static double area(Shape s) {
    return switch (s) {                 // no default: all three are covered
        case Circle c -> Math.PI * c.r() * c.r();
        case Square q -> q.side() * q.side();
        case Rect r -> r.w() * r.h();
    };
}

06Statements, blocks and fall-through

Pattern cases also work in switch statements, and a statement that uses patterns or case null must be exhaustive too (old-style statements over int or String still don't have to be).

A case can run a block; in a switch expression the block returns its value with yield. With the old colon form (case String s:) you can't fall through into a case that declares a binding, because the binding would be unassigned: javac reports illegal fall-through to a pattern.

Enum constants can be used qualified in a pattern switch over a sealed interface, for example case Coin.ONE -> 1; when Coin is an enum that implements the sealed interface.

07What happens at run time

A pattern switch compiles to a null check, an invokedynamic call to SwitchBootstraps.typeSwitch that finds the index of the first matching case, then a plain lookupswitch jumping to that case's code, where a checkcast fills the binding. The bootstrap builds the type tests once, at the first call.

If none matches (only possible when classes changed after compilation), the compiler-generated default throws MatchException. Here a new Triangle subtype was added and only Shape.java and Factory.java were recompiled:

terminal
$ javac Shape.java Factory.java
java Main
── expected output ──
Exception in thread "main" java.lang.MatchException
at Main.name(Main.java:3)
at Main.main(Main.java:9)

Try it yourself

  1. 1

    Reorder and read the error

    In the first example, move case Integer i -> ... above case Integer i when i < 0 -> .... Predict the compiler's message, compile, and check. Then put it back.

  2. 2

    Remove case null

    Delete the case null line from describe. Predict what happens for the last input. Run it and read the stack trace.

  3. 3

    Grow the sealed type

    In the shapes example, add record Triangle(double b, double h) implements Shape { } and add it to permits. Compile without touching the switches. Count the errors and fix each one.

Code & diagrams

Type patterns, guards and case null Java 21+ New tab
This example needs Java 21+. The in-browser compiler is Java 17: install JDK 21 or newer and run it with "java Main.java".

Expected output

a number 42
a negative number -7
a string of length 4
a blank string
an int array of 3
something else: Double
nothing at all
Exhaustive switch over a sealed interface Java 21+ New tab

Delete the `case Square q` line in area() and compile: the error lists the switch, not a runtime bug.

This example needs Java 21+. The in-browser compiler is Java 17: install JDK 21 or newer and run it with "java Main.java".

Expected output

Circle[r=1.0] area=3.14 circle
Square[side=3.0] area=9.0 square
Rect[w=2.0, h=5.0] area=10.0 rect
Rect[w=4.0, h=4.0] area=16.0 rect that is really a square
Circle[r=0.0] area=0.0 a dot
null: classic switch vs pattern switch Java 21+ New tab
This example needs Java 21+. The in-browser compiler is Java 17: install JDK 21 or newer and run it with "java Main.java".

Expected output

M no size
classic(null) threw NullPointerException
log text: started
log number: 3
log other
Qualified enum constants and case null, default Java 21+ New tab
This example needs Java 21+. The in-browser compiler is Java 17: install JDK 21 or newer and run it with "java Main.java".

Expected output

52
text / not text (or null) / not text (or null)

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Put a general case before a specific one

Write case CharSequence cs -> "chars"; above case String s -> "string";.

terminal
$ javac Main.java
── what you'll see ──
Main.java:5: error: this case label is dominated by a preceding case label
case String s -> "string";
^
1 error

Break #2

Forget a subtype of a sealed interface

In a switch over sealed interface Shape permits Circle, Square, write only case Circle c -> ....

terminal
$ javac Main.java
── what you'll see ──
Main.java:7: error: the switch expression does not cover all possible input values
return switch (s) {
^
1 error

Break #3

Compile pattern switches for an old release

Compile a switch with case String s -> using javac --release 17.

terminal
$ javac --release 17 Main.java
── what you'll see ──
Main.java:4: error: patterns in switch statements are not supported in -source 17
case String s -> "string";
^
(use -source 21 or higher to enable patterns in switch statements)
1 error

Myth vs fact

Myth

default catches null.

Fact

It doesn't. Without case null, a null selector throws NullPointerException, even in a pattern switch with default. Write case null, default to send null to the default branch.

Myth

A pattern switch is slow because it tests types one by one.

Fact

It compiles to an invokedynamic type switch built once at first call, plus a jump table. For a handful of cases it is comparable to an if/instanceof chain, and the JIT optimises both.

Myth

Exhaustive switches over sealed types are fragile because new subtypes break them.

Fact

That compile error is the point: it lists every place that must decide how to handle the new subtype. A default hides those places.

Myth

Pattern switches only work on sealed types.

Fact

They work on any reference type. Over a non-sealed type such as Object, you need a default (or a total pattern) for exhaustiveness.

Interview problem

The problem

Notification router

Your service sends three kinds of notifications: email, SMS and push. Each has different fields and a different delivery cost. Design the types and a cost function so that adding a fourth channel later can't be forgotten anywhere in the codebase.

You're given

  • Email has an address and a size in KB; cost is 1 paisa per 100 KB, minimum 1.
  • SMS has a phone number and a text; cost is 15 paise per 160-character segment.
  • Push has a device token; cost is 0, unless the token is blank (invalid), which must be rejected.

The interviewer follows up

01

Why not an enum Channel { EMAIL, SMS, PUSH }?

02

What happens if an old compiled service receives the new record at run time?

When it breaks

A default branch hides a new subtype

What you see

A team adds Refund to a payment hierarchy. A fee switch had default -> 0, so refunds are silently charged no fee and the reconciliation report is off for weeks, with no compile error and no exception.

Fix & prevent

Seal the hierarchy and remove default from switches over it, so new subtypes cause compile errors. Keep default only for open types like Object.

Partial redeploy after changing a sealed hierarchy

What you see

A shared library adds a subtype and one service is redeployed with it while another service's switch was compiled against the old version. The old service throws java.lang.MatchException on the first message carrying the new type.

Fix & prevent

Deploy consumers before producers start sending new types, version message schemas, and catch the exception at the message boundary to dead-letter rather than crash.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Exhaustiveness is checked at compile time but enforced again at run time: the compiler adds a synthetic default that throws MatchException (Java 21) for pattern switches. This protects against separate compilation, where a sealed hierarchy gains a subtype but the switch's class wasn't recompiled.

  • ▸

    SwitchBootstraps.typeSwitch receives the case labels as bootstrap arguments (classes, and constants such as strings or enum descriptors) and a restart index. When a guard fails, the generated code calls the bootstrap again with the next index, so matching continues from the following case without retesting earlier ones.

  • ▸

    Pattern switch is Java's answer to the Visitor pattern: instead of adding accept/visit methods to every class, you write one exhaustive switch per operation. Visitor still wins when the set of operations is closed and the types are open; sealed types plus switches win when the types are closed and operations grow (see the System Design course's behavioural patterns).

  • ▸

    Primitive types in patterns (case int i, switch on long, float, double, boolean) are a separate feature: preview in Java 23, 24 and 25 (JEP 455, 488, 507), not yet standard as of Java 25.

Remember this

  1. 1

    A classic switch (Topic 2.3) and a switch expression (Topic 2.4) compare a value against constants: numbers, characters, strings and enum names. Pattern matching for switch lets a case hold a pattern instead: case Integer i -> ... matches any Integer and binds it to i, exactly like o instanceof Integer i in Topic 11.1. The selector can be any reference type. It was previewed in Java 17 to 20 and became standard in Java 21 (JEP 441).

  2. 2

    A case can add a guard with when: case Integer i when i < 0 -> "negative". The case matches only if the pattern matches and the boolean guard is true. Guards replace nested if statements inside a case and can use the binding.

  3. 3

    Order matters. Cases are tried top to bottom, and the first match wins. The compiler rejects a case that can never be reached because an earlier one always catches its values: case CharSequence cs before case String s gives this case label is dominated by a preceding case label. Put specific cases (and guarded cases) before general ones.

  4. 4

    **null handling.** A classic switch throws NullPointerException when the selector is null. A pattern switch does the same unless it has a case null (or case null, default), in which case null goes there. default alone never catches null.

  5. 5

    Exhaustiveness. A pattern switch (expression or statement) must cover every possible value of the selector's type. Over Object that means a default (or a case Object o). Over a sealed interface (Topic 5.11) the compiler knows every permitted subtype, so covering each one is enough and no default is needed. Then, when someone adds a new subtype, every switch that forgot it fails to compile. That turns a whole class of runtime bugs into compile errors.

  6. 6

    At run time, javac turns a pattern switch into an invokedynamic call to java.lang.runtime.SwitchBootstraps.typeSwitch, which returns the index of the first matching case, followed by an ordinary lookupswitch. If a sealed hierarchy changed after your code was compiled and an unknown subtype arrives, the hidden default throws MatchException instead of silently doing nothing. Topic 11.3 adds record patterns, which take each case apart in the same step.

Explain it without notes

01

What does pattern matching for switch add over a classic switch, and in which version did it become standard?

02

What is a guard, and how does it interact with case order?

03

Explain dominance with an example.

04

How does a pattern switch treat null?

05

Why is an exhaustive switch over a sealed interface without default better than one with default?

Practice

01

Write static String classify(Object o) that returns "null", "even N"/"odd N" for Integers, "empty text" or "text 'S'" for Strings, and "other X" otherwise. Test with 4, 7, "", "dosa", 1.5, null.

02

Model sealed interface Payment permits Card, Upi, Cash as records with an amount in paise (Card also has boolean international). Write feePaise with no default: international cards 3%, domestic cards 2%, UPI and cash free.

03

Write static String toJson(Object o) with a pattern switch that handles null, String (quoted), Number, Boolean, and List<?> (recursively, using a block and yield), and throws IllegalArgumentException otherwise.

Trade-offs

  • ↔

    Exhaustive switches over sealed types catch forgotten cases at compile time, but each new subtype forces edits in every switch. That's right for a closed domain (payment kinds, AST nodes) and wrong for a plugin-style open set, where polymorphic methods fit better.

  • ↔

    Guards keep related logic in one place, but long guards turn a switch into hidden business rules. Move complex conditions into well-named methods: case Order o when o.isOverdue() -> ....

  • ↔

    Pattern switches need Java 21. Libraries that support older runtimes must keep if/instanceof chains or multi-release JARs.

Done when you can

  • Done when you can rewrite an instanceof chain as a pattern switch.

  • Done when you can order cases correctly and explain dominance errors.

  • Done when you know how null behaves with and without case null.

  • Done when you can write an exhaustive switch over a sealed interface without default.

  • Done when you can explain MatchException and when it is thrown.