Skip to content

Java · Modern Java

Java Switch Pattern Matching — Expressions, Type Patterns, and Record Deconstruction

9 min read Updated 2026-06-20 Share:

Practice Switch Pattern Matching interview questions

A brief history of switch in Java

Switch in Java evolved in stages:

  • Java 14 (JEP 361) — switch expressions: produce a value, arrow -> syntax, no fall-through.
  • Java 16 (JEP 394) — pattern matching for instanceof: bind a variable in one step.
  • Java 17–20 — switch pattern matching in preview (JEP 406, 420, 427, 433).
  • Java 21 (JEP 441) — switch pattern matching finalised: type patterns, guarded patterns, record deconstruction, null cases.

Understanding the full picture — from basic switch expressions to exhaustive pattern-matching switches — is what separates a strong modern Java answer from a mediocre one.

Switch expressions (Java 14)

A switch expression produces a value and can be used anywhere an expression is expected. The key differences from the traditional switch statement:

// Traditional switch statement:
String label;
switch (day) {
    case MONDAY:
    case TUESDAY:
        label = "early week";
        break;
    default:
        label = "later";
}

// Switch expression (Java 14):
String label = switch (day) {
    case MONDAY, TUESDAY -> "early week";
    default              -> "later";
};

Arrow labels (->) don't fall through and don't need break. Multiple labels per case are comma-separated. The expression must be exhaustive — the compiler rejects a switch expression that doesn't cover all possible values.

yield in block arms

When a case arm needs multiple statements, use a block with yield to return a value:

int fee = switch (tier) {
    case "gold"   -> 0;
    case "silver" -> 5;
    default -> {
        int base = 10;
        int penalty = tier.length();
        yield base + penalty;   // returns the value from this arm
    }
};

yield is to switch expressions what return is to methods — scoped to the expression, not the enclosing method.

Pattern matching for instanceof (Java 16)

Before Java 16, testing a type and then using it required two steps:

// Old way — redundant cast:
if (obj instanceof String) {
    String s = (String) obj;
    System.out.println(s.toUpperCase());
}

Pattern matching for instanceof (JEP 394) collapses the test and binding into one:

if (obj instanceof String s) {
    System.out.println(s.toUpperCase()); // s is in scope and typed
}

The bound variable s is in scope only when the pattern matches. It also works in compound expressions:

if (obj instanceof String s && s.length() > 5) {
    System.out.println("long string: " + s);
}

The compiler is smart enough to know s is only accessible in the && branch where the test succeeded.

Type patterns in switch (Java 21)

Switch pattern matching extends instanceof patterns to multi-way dispatch:

Object obj = getValue();

String desc = switch (obj) {
    case Integer i  -> "int: " + i;
    case Long l     -> "long: " + l;
    case String s   -> "str: " + s;
    case null       -> "null";
    default         -> "other: " + obj.getClass().getName();
};

This replaces chains of if (obj instanceof X x) ... else if (obj instanceof Y y) ... with a single, readable, exhaustiveness-checked construct. The selector can be any reference type — not just int, String, or enum as with traditional switch.

Guarded patterns — the when clause

A when clause adds a boolean condition to a type pattern:

String classify(Object obj) {
    return switch (obj) {
        case Integer i when i < 0   -> "negative";
        case Integer i when i == 0  -> "zero";
        case Integer i              -> "positive";
        case String s when s.isEmpty() -> "empty string";
        case String s               -> "non-empty string";
        default                     -> "unknown";
    };
}

Cases are tested top-to-bottom. The when guard is evaluated only if the type pattern matches first. Always put the most specific guards earlier — a later catch-all case for the same type handles the remaining values.

Exhaustiveness with sealed types

The most powerful feature: when the selector is a sealed type, the compiler verifies that every permitted subtype is handled. No default needed, and adding a new subtype breaks the switch at compile time, forcing you to update every exhaustive switch.

sealed interface Shape permits Circle, Rectangle {}
record Circle(double r)          implements Shape {}
record Rectangle(double w, double h) implements Shape {}

double area(Shape s) {
    return switch (s) {
        case Circle c        -> Math.PI * c.r() * c.r();
        case Rectangle r     -> r.w() * r.h();
    };
    // Compiler error if Triangle is added to Shape without handling it here
}

This is exhaustiveness you'd previously get only from enums — now available for any closed type hierarchy.

Record deconstruction in switch

Record patterns (Java 21, JEP 440) let you destructure a record's components directly in a case:

sealed interface Expr permits Num, Add, Mul {}
record Num(int v)           implements Expr {}
record Add(Expr l, Expr r)  implements Expr {}
record Mul(Expr l, Expr r)  implements Expr {}

int eval(Expr e) {
    return switch (e) {
        case Num(int v)           -> v;
        case Add(Expr l, Expr r)  -> eval(l) + eval(r);
        case Mul(Expr l, Expr r)  -> eval(l) * eval(r);
    };
}

System.out.println(eval(new Add(new Num(3), new Mul(new Num(4), new Num(5))))); // 23

Nested deconstruction also works — components that are themselves records can be destructured inline:

record Point(int x, int y) {}
record Segment(Point start, Point end) {}

String describe(Segment seg) {
    return switch (seg) {
        case Segment(Point(int x1, int y1), Point(int x2, int y2))
            -> "(%d,%d)→(%d,%d)".formatted(x1, y1, x2, y2);
    };
}

No accessor calls, no explicit casts — the structure is declared in the pattern itself.

Handling null explicitly

Traditional switch throws NullPointerException if the selector is null. Java 21 allows case null to handle it explicitly:

switch (input) {
    case null           -> System.out.println("got null");
    case "yes", "true"  -> System.out.println("affirmative");
    default             -> System.out.println("other: " + input);
}

You can combine: case null, default -> handles both null and unmatched values together. If no case null is present, null still throws NPE for backward compatibility.

Dominance — ordering matters

When using type patterns, more specific types must appear before more general ones. The compiler rejects dominated cases — cases that can never be reached because a prior case already matches every value:

// Compile error:
switch (obj) {
    case Object o -> System.out.println("any");   // dominates everything
    case String s -> System.out.println("string"); // unreachable!
}

// Correct — most specific first:
switch (obj) {
    case String s  -> System.out.println("string");
    case Integer i -> System.out.println("int");
    default        -> System.out.println("other");
}

Rule of thumb: order from most specific to most general, same as catch blocks.

Switch pattern matching vs polymorphism

Switch pattern matching is not a replacement for polymorphism — it's a complement:

SituationPrefer
Operation belongs to the type (area(), validate())Polymorphism
Operation is external (serialisation, logging, rendering)Switch patterns
Open hierarchy — third parties add subtypesPolymorphism
Closed hierarchy (sealed types)Switch patterns
Compile-time exhaustiveness requiredSwitch patterns

The classic trade-off: adding a new operation on a closed type is easy with switch (add one function); adding a new type variant to an open type is easy with polymorphism (add one class). Sealed + switch inverts this: new type variants break existing switches (good — the compiler guides you), but new operations require no changes to existing classes.

Real-world example: JSON value dispatch

sealed interface JsonValue permits
    JsonString, JsonNumber, JsonBool, JsonNull, JsonArray, JsonObject {}

record JsonString(String value)        implements JsonValue {}
record JsonNumber(double value)        implements JsonValue {}
record JsonBool(boolean value)         implements JsonValue {}
record JsonNull()                      implements JsonValue {}
record JsonArray(List<JsonValue> items) implements JsonValue {}
record JsonObject(Map<String, JsonValue> fields) implements JsonValue {}

String toDisplayString(JsonValue v) {
    return switch (v) {
        case JsonString(String s)  -> "\"" + s + "\"";
        case JsonNumber(double n)  -> n % 1 == 0
            ? String.valueOf((long) n) : String.valueOf(n);
        case JsonBool(boolean b)   -> String.valueOf(b);
        case JsonNull()            -> "null";
        case JsonArray(var items)  -> items.stream()
            .map(this::toDisplayString).collect(joining(",", "[", "]"));
        case JsonObject(var fields) -> fields.entrySet().stream()
            .map(e -> "\"" + e.getKey() + "\":" + toDisplayString(e.getValue()))
            .collect(joining(",", "{", "}"));
    };
}

The switch is exhaustive (all six JsonValue subtypes covered), the components are destructured inline, and adding a new JSON type to the sealed interface immediately breaks this method at compile time — intentional and safe.

Recap

Switch expressions (Java 14) produce values, use arrow syntax, and are exhaustively checked. Pattern matching for instanceof (Java 16) binds a typed variable in a single test. Switch pattern matching (Java 21) extends this to multi-way type dispatch with type patterns, guarded patterns (when), record deconstruction, and explicit case null handling. When the selector is a sealed type, the compiler verifies exhaustiveness — adding a new permitted subtype breaks every unhandled switch at compile time. Order cases from most to least specific to avoid dominance errors. This combination of sealed interfaces + records + switch pattern matching is the modern Java replacement for the Visitor pattern — less ceremony, compile-time safety, and no external visitor infrastructure.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel