skip to content

Pattern Matching in switch

Switch case labels can now be type patterns, refined by a when guard, with null handled explicitly rather than by NullPointerException. Interviewers combine it with sealed hierarchies to ask about exhaustiveness without a default.

part ofJavaoverview, primer and where to startread it →
on this pageshow

questions

5

What are type patterns in a switch, and how do they change what a switch can do compared to a traditional switch?

level: juniorimportance: must knowfreq 70%

answer

  1. case Type var -> ... (test + bind in one)
  2. Replaces instanceof + cast ladders
  3. Selector can be any reference type (Object)
  4. Must be exhaustive (default or all subtypes)
  5. First matching case in order wins

basics

~20 s

A type pattern in a switch case checks whether the value is of a given type and, if so, binds it to a new variable in one step. This lets a switch branch on the runtime type of an object, not just on constants like ints, enums, or strings.

solid answer

~40 s

Traditional switch could only test a value against constants (ints, chars, enums, strings) using equality. Type patterns let each case label be a type plus a binding variable, e.g. `case Integer i`. At runtime the switch checks if the selector is an instance of that type; if it is, the value is cast and bound to the named variable, ready to use in that branch. This turns switch into a tool for branching on an object's runtime type, eliminating long `if-else` chains of `instanceof` + cast. It works with the arrow form (`case Integer i -> ...`) so there is no fall-through, and the binding is scoped to its branch. Pattern switches must usually be exhaustive, and the selector can be any reference type, not just the few legacy switch types.

code

java · 8 lines
java
static String describe(Object obj) {
    return switch (obj) {
        case Integer i -> "integer: " + i;     // test + bind in one
        case String s  -> "string of length " + s.length();
        case int[] a   -> "int array of size " + a.length;
        default        -> "something else";
    };
}

go deeper

for a junior

Knows a type pattern case tests the runtime type and binds a typed variable, replacing instanceof + cast.

for a middle

Can write an exhaustive pattern switch over an Object, explains arrow form (no fall-through) and per-branch binding scope, and knows order matters.

for a senior

Articulates exhaustiveness rules, selector-type widening, interaction with sealed types, and migration from instanceof ladders; mentions null behavior.

for a principal

Frames pattern switch within Java's broader pattern-matching/data-oriented-programming direction (sealed types + records + switch), and reasons about API design and exhaustiveness as a maintainability guarantee.

## Background: what a switch used to be A `switch` is a control-flow statement that picks one branch based on a value (the *selector*). Historically Java's switch could only compare the selector against **constant labels** using equality: integral types (`int`, `byte`, `short`, `char`), their boxed forms, `enum` constants, and (since Java 7) `String`. Each `case` was a constant; the runtime asked "does the selector equal this constant?" ```java switch (day) { // day is an int or enum case MONDAY: ...; break; case TUESDAY: ...; break; } ``` ## What a *pattern* is A **pattern** is a test-and-extract construct: it asks "does this value match a certain shape?" and, if so, **binds** (assigns) part of the value to a variable. The simplest pattern is the **type pattern**: a type name followed by a variable name, e.g. `Integer i`. Matching a value `v` against the type pattern `Integer i` means: *is `v` an instance of `Integer`? If yes, declare a variable `i` of type `Integer` and assign `v` to it.* This is exactly what `instanceof` pattern matching does: ```java if (obj instanceof Integer i) { // tests AND binds i use(i); // i is in scope here } ``` ## Type patterns inside switch Pattern matching in switch (finalized in Java 21) lets a `case` label be a **type pattern** instead of a constant: ```java static String describe(Object obj) { return switch (obj) { case Integer i -> "int " + i; // matches if obj is an Integer; binds i case String s -> "string " + s; // binds s case int[] a -> "array len " + a.length; default -> "other"; }; } ``` At runtime the switch evaluates the cases **in order**; the first whose type pattern the selector is an instance of wins. The matched value is cast to that type and bound to the case's variable, which is then usable in that branch only. ## Why this matters Without it, branching on runtime type meant a chain of `instanceof` + cast: ```java if (obj instanceof Integer) { Integer i = (Integer) obj; ... } else if (obj instanceof String) { String s = (String) obj; ... } ``` The pattern switch replaces this with a single, exhaustive, readable structure and removes the redundant casts. ## Key rules that come with it - **Exhaustiveness:** a switch that uses patterns (or has a result, like a switch *expression*) must cover all possible inputs. If you do not list every type, you must add a `default` (or, for sealed hierarchies, all permitted subtypes). The compiler enforces this. - **The selector type widens:** the selector may be any reference type (e.g. `Object`), not just the legacy switch types. - **Arrow form:** `case Label -> expr;` has no fall-through and scopes bindings to the branch. The old colon form (`case Label:`) still exists but is more error-prone with patterns. - **null:** a pattern switch does **not** silently accept `null`; by default `null` throws `NullPointerException` unless you add `case null` (covered separately). ## Deriving an answer From first principles: switch = pick a branch by value; a type pattern = "is it this type, and if so give me a typed variable"; combine them = a switch that branches on runtime type and hands you a ready-to-use, correctly-typed variable in each branch, replacing `instanceof`/cast ladders.

  • Does a pattern switch need to be exhaustive, and what happens if it is not?
    Yes. A switch that uses patterns (and any switch expression) must cover all inputs. If it does not list every possibility, the compiler requires a `default` (or all permitted subtypes of a sealed type); otherwise it is a compile error.
  • Is the bound variable visible outside its case?
    No. With the arrow form the binding is scoped to that single branch, so different cases can reuse the same name and there is no leakage.

saying these in an interview costs you the question

  • Thinking switch can still only handle int/enum/String — patterns allow any reference type
  • Forgetting the binding variable is typed and scoped to its branch
  • Assuming you still need a manual cast after the case matches
  • Believing case order does not matter (it does — first match wins)

context

open as a page

What is a guarded pattern (the `when` clause) in a switch, and how does it interact with case ordering and exhaustiveness?

level: middleimportance: must knowfreq 60%

basics

~20 s

A guarded pattern adds a boolean condition to a case using when, e.g. case Integer i when i > 0. The case matches only if the value fits the pattern AND the condition is true. If the guard is false, the switch keeps trying later cases.

open as a page

How does a pattern switch handle a null selector, and what does `case null` (and `case null, default`) do?

level: middleimportance: should knowfreq 55%

basics

~20 s

By default a switch throws NullPointerException if the value is null. To handle null safely you add case null, which runs only when the value is null. You can also write case null, default to send null to the default branch.

open as a page

How does exhaustiveness work in a pattern switch, and how do sealed types let you write a switch with no default?

level: seniorimportance: should knowfreq 50%

basics

~20 s

A pattern switch must handle every possible value (be exhaustive). If you switch over a sealed type, the compiler knows all its allowed subtypes, so once you cover each one you don't need a default — and if someone adds a new subtype later, the switch stops compiling until you handle it.

open as a page

Walk through refactoring an `if-else` chain of `instanceof` + cast into a pattern switch. What are the pitfalls (dominance, fall-through, null) to watch for?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Replace each if (x instanceof T t) branch with a case T t -> label in a switch over x. Use guards (when) for the extra conditions, keep the most specific cases first, add case null if null is possible, and make it exhaustive with a default or full subtype coverage.

open as a page