Java pattern matching for switch lets a case test the runtime type and structure of a value. When a pattern matches, Java also creates variables that can be used directly by that case.
The feature became permanent in Java 21. It makes data-oriented branching more concise than a long sequence of instanceof checks and casts.
Match Values by Type
A type pattern consists of a type followed by a pattern variable. The variable is initialized only when the selected value matches that type.
Example
static String describe(Object value) {
return switch (value) {
// Each successful pattern creates a typed variable
case Integer number -> "Integer: " + number;
case Double number -> "Double: " + number;
case String text -> "Text: " + text;
default -> "Other: " + value;
};
}
No explicit cast is needed. Inside the String rule, for example, text already has the type String.
Use a switch Expression or Statement
Pattern labels work in both switch expressions and switch statements. A switch expression returns a value and must be exhaustive.
Switch expression
static int textLength(Object value) {
// Every possible non-null type has a matching rule
return switch (value) {
case CharSequence text -> text.length();
default -> 0;
};
}
A switch statement performs actions instead of producing a value. A statement that uses pattern or null labels must also be exhaustive.
Switch statement
static void printType(Object value) {
switch (value) {
// Arrow rules avoid accidental fall-through
case String text -> System.out.println("String: " + text);
case Number number -> System.out.println("Number: " + number);
default -> System.out.println("Another reference type");
}
}
Handle null Inside switch
A case null label handles a null selector directly. Without it, switching on a null reference still throws NullPointerException. The default label alone does not match null.
Example
static String normalize(Object value) {
return switch (value) {
// Handle null without a separate if statement
case null -> "missing";
case String text -> text.trim();
default -> value.toString();
};
}
You can combine the remaining null and unmatched cases when they perform the same action.
Example
static String category(Object value) {
return switch (value) {
case Integer number -> "whole number";
case Double number -> "decimal number";
// Match null and every otherwise unmatched value
case null, default -> "other";
};
}
Refine a Pattern with when
A guarded pattern adds a boolean condition with when. The pattern must match and its guard must evaluate to true.
Example
static String classify(Object value) {
return switch (value) {
// Test the type and an additional condition
case String text when text.isBlank() -> "blank text";
case String text when text.length() < 10 -> "short text";
case String text -> "long text";
case null -> "missing";
default -> "not text";
};
}
The pattern variable is in scope within the guard and the corresponding rule. Guards belong to pattern labels; a constant case label cannot independently use a when guard.
Order Cases from Specific to General
More than one type pattern may theoretically match the same value. Java uses the first applicable label and rejects a label that is dominated by an earlier label.
Incorrect order
static void invalidOrder(Object value) {
switch (value) {
// CharSequence already matches every String
case CharSequence text -> System.out.println(text.length());
// Compile-time error: this case is dominated
case String text -> System.out.println(text.toUpperCase());
default -> System.out.println("Other");
}
}
Place constants first, guarded patterns next, and broader unguarded patterns last.
Correct order
static String inspect(Object value) {
return switch (value) {
// Special and guarded cases come before the general type
case "admin" -> "reserved name";
case String text when text.isBlank() -> "blank";
case String text -> "text: " + text;
default -> "other";
};
}
Tip: Think of pattern case ordering like exception handlers: place narrower cases before cases that accept a wider set of values.
Write an Exhaustive Pattern switch
A pattern switch must cover every possible selector value. A match-all default or unconditional type pattern can provide coverage for an open type such as Object.
Example
static int sizeOf(Object value) {
return switch (value) {
case null -> 0;
case String text -> text.length();
case int[] values -> values.length;
// Covers all remaining non-null Object values
case Object ignored -> 1;
};
}
An unconditional pattern that covers the selector type acts as a match-all rule. It cannot appear together with a separate default because both would cover the same remaining values.
Use Sealed Types for Complete Coverage
When a selector has a sealed type, the compiler knows its permitted direct subtypes. A switch can cover those alternatives without a default rule.
Example
sealed interface Payment permits CardPayment, CashPayment {
}
record CardPayment(double amount, String lastFour)
implements Payment {
}
record CashPayment(double amount)
implements Payment {
}
static double amountOf(Payment payment) {
// All permitted Payment implementations are covered
return switch (payment) {
case CardPayment card -> card.amount();
case CashPayment cash -> cash.amount();
};
}
Omitting an unnecessary default helps the compiler report a missing case after a new permitted subtype is added and the switch is recompiled.
Combine switch with Record Patterns
Record patterns, also permanent since Java 21, can deconstruct a record directly in a case label. This combines type testing, casting, and component extraction.
Example
sealed interface Shape permits Circle, Rectangle {
}
record Circle(double radius) implements Shape {
}
record Rectangle(double width, double height) implements Shape {
}
static double area(Shape shape) {
return switch (shape) {
// Extract record components directly in each case
case Circle(double radius) ->
Math.PI * radius * radius;
case Rectangle(double width, double height) ->
width * height;
};
}
Use Nested Record Patterns
A record component can itself be matched by another record pattern.
Example
record Point(int x, int y) {
}
record Line(Point start, Point end) {
}
static String describeLine(Line line) {
return switch (line) {
// Deconstruct Line and both nested Point values
case Line(Point(int x1, int y1), Point(int x2, int y2))
when x1 == x2 -> "Vertical line";
case Line(Point(int x1, int y1), Point(int x2, int y2))
when y1 == y2 -> "Horizontal line";
case Line(Point start, Point end) ->
"Diagonal from " + start + " to " + end;
};
}
Pattern Variable Scope
A pattern variable exists only where Java knows the pattern matched:
- Inside the guard belonging to its case.
- Inside the expression, statement, or block to the right of an arrow rule.
- Inside the statements belonging to a colon-style case group.
- Not inside other case rules or after the switch.
Arrow rules are usually clearer because each pattern variable has an obvious, isolated scope and accidental fall-through is impossible.
Important Rules
| Rule | Meaning |
|---|---|
| Compatibility | Every pattern must be compatible with the selector's declared type. |
| Dominance | An earlier case cannot make a later case unreachable. |
| Exhaustiveness | Every possible value must be covered by a pattern switch. |
| Null handling | Null requires a null label or causes NullPointerException. |
| Guard evaluation | A guard runs only after its pattern has matched. |
| Variable scope | A pattern variable is available only where the match is known to succeed. |
Best Practices
- Order constants and narrow patterns before broader type patterns.
- Use guards for short conditions directly related to the pattern.
- Handle null explicitly when null is a valid input.
- Avoid a default rule for sealed hierarchies when listing every subtype is practical.
- Use polymorphic methods instead when behavior naturally belongs to each class.
- Prefer arrow rules to prevent fall-through and clarify variable scope.
Conclusion
Java pattern matching for switch provides concise type dispatch, scoped variables, guards, explicit null handling, and compile-time completeness checks. It is especially effective with sealed hierarchies and record patterns, provided cases are ordered from specific to general and the switch remains exhaustive.