Pattern Matching for Switch in Java
Pattern matching for switch lets a switch choose a case based on the type of a value, not just on a fixed constant. Each case can hold a pattern such as case String s, which tests whether the value is a String and, if so, gives you a variable s of that type. It became a standard feature in Java 21 (JEP 441). It replaces long chains of if (o instanceof ...) else if (o instanceof ...) with one clear, checked switch. In this lesson, you will learn the syntax, the rules for ordering cases, why the compiler insists on covering every value, and how the JVM runs it.
Before and After
Without pattern matching, testing several types means an instanceof chain:
static String describe(Object o) {
if (o instanceof Integer i) {
return "an Integer: " + (i * 2);
} else if (o instanceof String s) {
return "a String of length " + s.length();
} else {
return "something else";
}
}With pattern matching for switch, the same logic is one expression:
static String describe(Object o) {
return switch (o) {
case Integer i -> "an Integer: " + (i * 2);
case String s -> "a String of length " + s.length();
default -> "something else";
};
}The switch version is shorter, and, more importantly, the compiler checks it: it rejects cases that can never be reached and switches that miss some values.
Syntax
switch (selector) {
case Type variable -> body;
case Type variable when condition -> body;
case null -> body;
default -> body;
}| Part | Meaning |
|---|---|
selector | Any reference type: Object, an interface, a class, an enum, a record, a sealed type |
case Type variable | A type pattern: matches if the selector is an instance of Type, and binds variable |
when condition | A guard: an extra condition (see "Guards in Switch (When Clause)") |
case null | Handles null (see "Handling Null in Switch") |
default | Matches anything not matched earlier |
Example: Describing Any Object
Let's see an example that handles several unrelated types, including an array and a generic List:
import java.time.LocalDate;
import java.util.List;
public class Describe {
static String describe(Object o) {
return switch (o) {
case Integer i -> "an Integer: " + (i * 2);
case String s -> "a String of length " + s.length();
case int[] arr -> "an int array of size " + arr.length;
case List<?> list -> "a List with " + list.size() + " items";
case LocalDate d -> "a date in " + d.getYear();
default -> "something else: " + o.getClass().getSimpleName();
};
}
public static void main(String[] args) {
System.out.println(describe(21));
System.out.println(describe("Navika"));
System.out.println(describe(new int[] {1, 2, 3}));
System.out.println(describe(List.of("a", "b")));
System.out.println(describe(LocalDate.of(2026, 11, 13)));
System.out.println(describe(3.5));
}
}an Integer: 42
a String of length 6
an int array of size 3
a List with 2 items
a date in 2026
something else: DoubleExplanation: The switch tests the cases from top to bottom and runs the first one that matches. In each case, the pattern variable already has the right type, so i * 2, s.length() and arr.length need no cast. 3.5 is a Double, which no case matches, so default runs. Generic types are written with a wildcard, List<?>, because the element type isn't known at run time.
Case Order and Dominance
Because the first match wins, a more general case placed before a more specific one would hide it. The compiler rejects this; it says the later case is dominated:
return switch (o) {
case CharSequence cs -> "text";
case String s -> "string"; // every String is a CharSequence
default -> "other";
};E2.java:5: error: this case label is dominated by a preceding case label
case String s -> "string";
^
1 errorThe same check applies to constants: a case 5 after case Integer n can never run, and is rejected with the same message. The fix is to put specific cases first, general ones last.
Every Value Must Be Covered
A pattern switch must be exhaustive: every possible value of the selector must match some case. For a selector of type Object, that normally means adding default. Without it:
E3.java:3: error: the switch expression does not cover all possible input values
return switch (o) {
^
1 errorThis rule applies to pattern switch statements too, not only expressions:
S1.java:3: error: the switch statement does not cover all possible input values
switch (o) {
^
1 errorOld-style switch statements over int, String or enums without patterns don't need to be exhaustive, for backward compatibility. With sealed types, you can cover every case without default; see "Exhaustive Switch with Sealed Types".
Mixing Enum Constants and Patterns
Since Java 21, a case label can name an enum constant with its type, such as Coin.ONE. This lets one switch handle enum constants and record types that implement the same sealed interface:
public class Money {
sealed interface Currency permits Coin, Note { }
enum Coin implements Currency { ONE, TWO, FIVE }
record Note(int value) implements Currency { }
static String value(Currency c) {
return switch (c) {
case Coin.ONE -> "1 rupee coin";
case Coin.TWO, Coin.FIVE -> "bigger coin";
case Note n -> n.value() + " rupee note";
};
}
public static void main(String[] args) {
System.out.println(value(Coin.ONE));
System.out.println(value(Coin.FIVE));
System.out.println(value(new Note(500)));
}
}1 rupee coin
bigger coin
500 rupee noteExplanation: The selector type is the sealed interface Currency. The three Coin constants and the Note record cover every possible value, so no default is needed.
No Fall-Through into Patterns
With the old colon (:) style, a case without break falls through to the next one. That isn't allowed into a case that declares a pattern variable, because the variable would never have been set:
E4.java:6: error: illegal fall-through to a pattern
case Integer i:
^
(the previous case label is missing a break)
1 errorUse the arrow (->) form with patterns; it never falls through.
Under the Hood: How a Pattern Switch Compiles
A constant switch compiles to a jump table. A pattern switch can't, because the JVM must test types. Instead, javac emits an invokedynamic call to a method named typeSwitch in java.lang.runtime.SwitchBootstraps. Here is the real javap output for this method:
static String d(Object o) {
return switch (o) {
case Integer i when i > 10 -> "big";
case Integer i -> "int";
case String s -> "str";
default -> "other";
};
} 2: invokestatic #7 // Method java/util/Objects.requireNonNull:(Ljava/lang/Object;)Ljava/lang/Object;
11: invokedynamic #13, 0 // InvokeDynamic #0:typeSwitch:(Ljava/lang/Object;I)I
16: tableswitch { // 0 to 2
0: 44
1: 68
2: 79
default: 90
}
44: aload_1
45: checkcast #17 // class java/lang/Integer
...
BootstrapMethods:
0: #63 REF_invokeStatic java/lang/runtime/SwitchBootstraps.typeSwitch:(...)
Method arguments:
#17 java/lang/Integer
#17 java/lang/Integer
#27 java/lang/StringExplanation:
Objects.requireNonNull: with nocase null, anullselector throwsNullPointerExceptionright away.typeSwitch: given the object and a starting index, it returns the number of the first case whose type matches: 0, 1 or 2 (the case labelsInteger,Integer,Stringlisted under "Method arguments"), or 3 for none.tableswitch: an ordinary jump table on that number.- The guard: if
i > 10is false, the code callstypeSwitchagain with the next index, so matching continues from the following case.
The bootstrap method runs once per call site; the JVM then optimizes the generated code, so pattern switches are fast.
Common Mistakes
Putting a general case before a specific one. The compiler reports "dominated"; reorder the cases.
Forgetting default when switching on Object. The switch must cover every value.
Expecting a pattern switch to handle null. Without case null, it throws NullPointerException.
Writing List<String> in a case. Generic type arguments are erased at run time; use List<?>.
Interview Corner
What is pattern matching for switch?
Since Java 21, case labels can contain patterns such as case String s, which test the selector's type and bind a variable of that type. Cases are tested top to bottom; the first match wins. The compiler rejects dominated cases (a case that can never be reached because an earlier one covers it) and requires pattern switches to be exhaustive, usually with default or a sealed type. It also supports guards (when), case null, record patterns, and qualified enum constants.
How does a pattern switch work at the bytecode level?
javac compiles it to an invokedynamic call bootstrapped by SwitchBootstraps.typeSwitch, which receives the case types as static arguments. At run time it returns the index of the first matching case, and a tableswitch jumps to that case's code. When a guard fails, the code calls typeSwitch again starting from the next index. Without case null, a null selector is rejected first with Objects.requireNonNull.
Practice
- Easy: Rewrite an
if/else ifchain that testsInteger,LongandDoubleinto a patternswitchthat returns each value doubled as aString. - Harder: Compile
T.javaabove and runjavap -c -v T. Find thetypeSwitchcall and the list of types it receives, then add acase Long land see how the list changes.
Key Takeaways
Was this tutorial helpful?