Java · Java 21 Features (18 to 21)

Java 21: Pattern Matching for switch and Record Patterns

Type patterns, guards and case null in switch, dominance and exhaustiveness rules, record patterns that deconstruct and nest, and data-oriented programming with sealed records.

  • 6 min read
  • 12 practice questions
  • 1 graded problem
On this page
  1. Description
  2. Pattern matching in a switch
  3. The four rules
  4. Record patterns
  5. Putting it together
  6. When to use this style
  7. Common mistakes
  8. Check yourself
  9. Key takeaways
  10. Quick revise
  11. Practice

Description

A lot of code has to handle several kinds of something: a payment result that's approved, declined or under review; a shape that's a circle or a rectangle; a message that's a text, an image or a file. Before Java 21, that code usually looked like this:

double area(Shape s) {
    if (s instanceof Circle c) return Math.PI * c.r() * c.r();
    else if (s instanceof Rect r) return (r.x2() - r.x1()) * (r.y2() - r.y1());
    else throw new IllegalArgumentException("unknown shape " + s);
}

It works until someone adds a Triangle: the code still compiles, and fails at run time with that exception. Java 21's pattern matching for switch and record patterns, together with the sealed types from Java 17, turn that run-time failure into a compile error. The compiler checks that every kind is handled, and you can pull a record apart in the same line that matches it.

Here's the path, from easiest to most technical:

  1. Pattern matching in a switch: matching on types.
  2. The four rules: order, null, guards, and covering every case.
  3. Record patterns: matching and unpacking in one step.
  4. Putting it together: a closed set of cases, handled exhaustively.
  5. When to use this style, and when a method on each class is better.

Pattern matching in a switch

A switch can now match on types, binding a variable of that type when it matches:

String describe(Object o) {
    return switch (o) {
        case null -> "nothing";
        case Integer i when i > 100 -> "big number";
        case Integer i -> "number " + i;
        case String s -> "text of " + s.length() + " characters";
        default -> "something else";
    };
}
// describe(null)     -> nothing
// describe(250)      -> big number
// describe(7)        -> number 7
// describe("Austin") -> text of 6 characters
// describe(2.5)      -> something else

case Integer i matches when o is an Integer and names it i. A guard (when i > 100) adds a condition.

The four rules

1. The first matching case wins, so put specific cases first. If an earlier case already catches everything a later one would, the later one can never run, and Java refuses to compile it: case CharSequence cs before case String s is an error ("this case label is dominated by a preceding case label"). Guarded and more specific cases go above general ones.

2. null needs its own case. Without case null, a null value makes the switch throw a NullPointerException, as switches always have. With case null ->, it's just another case.

3. Guards don't count toward covering everything. The compiler can't know whether a guard is ever true, so a guarded case only covers some values of its type. If you have case Circle c when c.r() > 10, you also need a plain case Circle c (or a default) for the small circles.

4. A pattern switch must cover every possible value. You can cover everything with a default, or, for a sealed type, by listing every permitted subtype. This applies to pattern switch statements too, not only expressions.

That last rule is where the safety comes from. Over a sealed type, leave default out and list every subtype. Then adding a new subtype turns every switch that doesn't handle it into a compile error, a to-do list from the compiler. A default would quietly absorb the new kind.

Record patterns

A record pattern matches a record and unpacks its components at the same time:

record Point(int x, int y) {}
record Rect(Point topLeft, Point bottomRight) implements Shape {}
record Circle(Point center, int r) implements Shape {}

int width(Shape s) {
    return switch (s) {
        case Circle(Point center, int r) -> 2 * r;
        case Rect(Point(var x1, var y1), Point(var x2, var y2)) -> x2 - x1;    // patterns nest
    };
}

if (o instanceof Point(int x, int y)) { ... }      // works with instanceof too
  • Components are matched by position, with their types or var.
  • Patterns nest, so a Rect made of two Points unpacks all the way down in one case.
  • A nested pattern like Point(var x1, var y1) doesn't match a null component, so a Rect whose first point is null falls through to the next case.
  • The unnamed pattern _, for components you don't need (case Circle(_, int r)), was a preview in Java 21 and became final in Java 22.

Putting it together

Sealed interfaces, records and pattern switches combine into a clean way to model "one of several kinds, each with its own data":

sealed interface PaymentResult permits Approved, Declined, NeedsReview {}
record Approved(String authCode) implements PaymentResult {}
record Declined(String reason) implements PaymentResult {}
record NeedsReview(int riskScore) implements PaymentResult {}

String message(PaymentResult r) {
    return switch (r) {
        case Approved(var code) -> "Paid (" + code + ")";
        case Declined(var reason) -> "Declined: " + reason;
        case NeedsReview(var score) when score >= 80 -> "Blocked for review";
        case NeedsReview n -> "Pending review";
    };                        // no default: adding a fourth result type won't compile until it's handled
}

This style is often called data-oriented programming: model the data as a closed set of records, and write each operation as one switch over them.

When to use this style

It's not a replacement for ordinary polymorphism. The question is which side you expect to grow:

You expect to add more...Prefer
operations (render, store, audit, price...) over a stable set of kindsa sealed type and pattern switches: one new switch per operation
kinds (new subclasses) with a stable set of operationsa method on each class: one new class per kind

Interviewers like this question, and the answer is "it depends on which axis grows".

Common mistakes

MistakeWhat happensDo this instead
A general case above a specific onecompile error: dominatedspecific and guarded cases first
No case null where null can occurNullPointerExceptionadd case null ->
Only guarded cases for a typenot exhaustive: doesn't compileadd an unguarded case for that type
default in a switch over your own sealed typenew kinds slip through silentlylist every subtype, no default
A nested pattern and a null componentthat case doesn't matchadd a fallback case for the record
_ on Java 21 without preview flagsdoesn't compilename the variable, or use Java 22+

Check yourself

Try to answer before opening each one. Recalling beats re-reading.

1. Why doesn't case CharSequence cs -> ...; case String s -> ...; compile?

Show answer

Every String is a CharSequence, so the second case could never match: it's dominated by the first. Put the more specific case first.

2. A switch over a sealed Shape has case Circle c when c.r() > 10 and case Square q. Why doesn't it compile?

Show answer

Guarded cases don't count toward covering every value: a Circle with a small radius would match nothing. Add a plain case Circle c.

3. What happens when a pattern switch with no case null gets a null?

Show answer

It throws a NullPointerException. Add case null -> to handle it.

4. Why leave out default in a switch over your own sealed interface?

Show answer

So the compiler checks that every permitted subtype is handled. When someone adds a new subtype, every switch that doesn't handle it stops compiling, instead of a default quietly handling it the wrong way.

Key takeaways

  • A pattern switch matches on types, binds variables, and can add when guards; the first match wins.
  • Specific cases first; case null if null can occur; guarded cases don't cover their type.
  • Over a sealed type, list every subtype and skip default, so new kinds become compile errors.
  • Record patterns match and unpack in one step, and they nest.
  • Sealed types plus records plus pattern switches suit a stable set of kinds with growing operations.

Quick revise

The whole skill on one page, for the night before an interview.

Everything from the lesson and the deep dive on one page. Skim it before practice or an interview, and open the full lesson for anything that doesn't ring a bell.

Pattern switch

return switch (o) {
    case null -> "nothing";                          // else null throws NullPointerException
    case Integer i when i > 100 -> "big";            // guarded, specific first
    case Integer i -> "number " + i;
    case String s -> "text";
    default -> "other";                              // or cover every subtype of a sealed type
};

Record patterns

case Circle(Point c, int r) -> 2 * r;
case Rect(Point(var x1, var y1), Point(var x2, var y2)) -> x2 - x1;   // nested
if (o instanceof Point(int x, int y)) { ... }
case Circle(_, int r) -> ...;                                         // _ is final in 22, preview in 21

The rules

RuleConsequence
First match winsspecific and guarded cases go first
An earlier case covering a later onecompile error: dominated
Guards don't count for coverageadd an unguarded case per type
Pattern switches must be exhaustivedefault, or every sealed subtype
No default over your sealed typesnew subtypes become compile errors

The traps

  • case CharSequence above case String.
  • No case null.
  • Only guarded cases for a type.
  • default hiding new sealed subtypes.
  • null components don't match nested patterns.

Staff-level points, one line each

  • Compiled to invokedynamic SwitchBootstraps.typeSwitch + tableswitch; false guards restart from the next case.
  • Exhaustive ≠ total: the remainder (null components, separately compiled new subtypes or constants) throws MatchException.
  • Nested exhaustiveness checks combinations: Pair(Some, Some), Pair(Some, None), Pair(None, Opt) covers all.
  • Qualified enum constants mix with patterns (case Speed.EXPRESS ->); listing every constant counts as covering the enum.
  • Unguarded dominates guarded below it (error); guarded cases never dominate each other (javac can't see overlapping guards).
  • Expression problem: OO makes new kinds easy; data-oriented makes new operations easy. Pattern switches replace Visitors.
  • After 21: _ final in 22; string templates withdrawn in 23; primitive patterns previewing.

Say it in an interview

  • "I'd make the results a sealed interface of records, so the compiler checks every switch over them."
  • "No default here: if someone adds a result type, I want every switch to stop compiling."
  • "It compiled as exhaustive, but a null component lands in the remainder and throws MatchException."

Practice

This skill comes with 12 multiple-choice questions that tell you which misconception a wrong answer reveals and 1 graded problem. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice Java 21: Pattern Matching for switch and Record Patterns free

Payment Result Messages

MediumProCoding problemJava

A checkout service in Atlanta turns the result of each payment into a message for the customer. The results are a sealed interface of records (given):

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Practice Java 21: Pattern Matching for switch and Record Patterns free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime