Comprehensive Guide to Pattern Matching for switch in Java
(including Guarded Patterns)
Target Java versions:
- Preview: Java 17 (JEP 406), Java 18 (JEP 420), Java 19 (JEP 427), Java 20 (JEP 433)
- Final / Standard: Java 21 (JEP 441 – Pattern Matching for switch)This guide covers all core aspects of pattern matching for
switchin modern Java,
including type patterns, record patterns, null handling, and guarded patterns usingwhen.
1. What Is Pattern Matching for switch?
Traditionally, switch in Java:
- Worked only with primitive types,
String, and enums. - Could not use
instanceof-style type tests. - Could not destructure structured objects.
- Was prone to fall-through errors.
Pattern Matching for switch (Java 21+) modernizes it by allowing:
- Patterns in
caselabels: - Type patterns:
case String s -> ... - Record patterns:
case Point(int x, int y) -> ... - Constant patterns:
case 1 -> ...,case "OK" -> ... nullpattern:case null -> ...- Guarded patterns using
when: case String s when s.length() > 5 -> ...- Exhaustiveness checking with sealed types.
- Integration with switch expressions (
switch (...) { ... }) and switch statements.
The result is a more expressive and safer way to branch on values and structures.
2. Basic Pattern Matching switch Syntax
2.1. Type Pattern Example
String describe(Object obj) {
return switch (obj) {
case String s -> "String of length " + s.length();
case Integer i -> "Integer value " + i;
case null -> "null";
default -> "Something else";
};
}
case String s ->is a type pattern:- Tests if
obj instanceof String - Binds
sto the casted value ((String) obj)
2.2. Switch Expression vs Switch Statement
Switch expression (returns a value):
String result = switch (obj) {
case String s -> "String: " + s;
case Integer i -> "Int: " + i;
default -> "Other";
};
Switch statement (side effects only):
switch (obj) {
case String s -> System.out.println("String: " + s);
case Integer i -> System.out.println("Int: " + i);
default -> System.out.println("Other");
}
Both support pattern matching and guarded patterns.
3. Kinds of Patterns in switch
3.1. Constant Patterns
Match fixed constant values:
switch (status) {
case 200 -> System.out.println("OK");
case 404 -> System.out.println("Not Found");
case 500 -> System.out.println("Server Error");
default -> System.out.println("Other");
}
3.2. Type Patterns
Match based on type and bind a variable:
switch (obj) {
case String s -> System.out.println("String length: " + s.length());
case Number n -> System.out.println("Numeric value: " + n);
default -> System.out.println("Other type");
}
3.3. Record Patterns (Java 21+)
Given:
record Point(int x, int y) {}
You can destructure:
switch (obj) {
case Point(int x, int y) -> System.out.println("Point: " + x + ", " + y);
default -> System.out.println("Not a Point");
}
Record patterns can be nested (see §7).
3.4. null Pattern
Java 21 pattern matching makes switch null-aware:
switch (obj) {
case null -> System.out.println("It was null");
case String s -> System.out.println("String: " + s);
default -> System.out.println("Other");
}
If you do not handle null:
switchwith a null selector will throw NullPointerException.
4. Guarded Patterns (when Keyword)
4.1. Syntax
Guarded patterns refine a match with an extra boolean condition:
case <pattern> when <boolean-expression> -> ...
Example:
String describe(Object obj) {
return switch (obj) {
case String s when s.length() > 5 -> "Long string";
case String s -> "Short string";
default -> "Not a string";
};
}
- First case: only matches Strings longer than 5 characters
- Second case: matches all other Strings
Important:
whenis the correct guard keyword inswitch.
case String s && s.length() > 5is not valid syntax.
4.2. Guard Semantics
case P when C -> ...
Steps:
- Try to match pattern
P - If match → bind pattern variables (e.g.,
s) - Evaluate
C(the guard) - If
Cistrue→ case matches - If
Cisfalse→ fallthrough; consider next case
Example:
switch (obj) {
case Integer i when i > 0 -> "Positive";
case Integer i when i == 0 -> "Zero";
case Integer i -> "Negative";
default -> "Not an integer";
}
5. Pattern Matching vs instanceof in if
Patterns also work with instanceof:
if (obj instanceof String s && s.length() > 5) {
System.out.println("Long string: " + s);
}
Key difference:
ifuses:obj instanceof String s && conditionswitchuses:case String s when condition ->
| Context | Syntax |
|---|---|
if |
if (x instanceof T t && condition) |
switch |
case T t when condition -> ... |
6. Exhaustiveness and Sealed Types
Pattern matching works incredibly well with sealed classes/interfaces.
6.1. Sealed Shape Hierarchy Example
sealed interface Shape permits Circle, Rectangle, Square {}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
record Square(double side) implements Shape {}
6.2. Exhaustive Switch
double area(Shape s) {
return switch (s) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
case Square sq -> sq.side() * sq.side();
}; // no default needed
}
- Because
Shapeis sealed and all permitted subtypes are covered,switchis exhaustive. - If a new subtype is added (e.g.,
Triangle), this switch will fail to compile until updated.
7. Record Patterns & Nested Patterns
Record patterns allow deconstruction and nesting.
7.1. Simple Record Pattern
record Point(int x, int y) {}
record Line(Point from, Point to) {}
String describe(Object obj) {
return switch (obj) {
case Point(int x, int y) -> "Point " + x + "," + y;
default -> "Other";
};
}
7.2. Nested Record Pattern
String describeLine(Object obj) {
return switch (obj) {
case Line(Point(int x1, int y1), Point(int x2, int y2)) ->
"Line from (" + x1 + "," + y1 + ") to (" + x2 + "," + y2 + ")";
default ->
"Not a line";
};
}
7.3. Nested + Guarded
String describeSpecialLine(Object obj) {
return switch (obj) {
case Line(Point(int x1, int y1), Point(int x2, int y2))
when x1 == x2 && y1 == y2 -> "Degenerate line (point)";
case Line(Point(int x1, int y1), Point(int x2, int y2)) ->
"Line from (" + x1 + "," + y1 + ") to (" + x2 + "," + y2 + ")";
default ->
"Not a line";
};
}
8. Dominance Rules (Case Order & Reachability)
The compiler checks that cases are not unreachable due to earlier, more general cases.
8.1. Example of Dominance Error
switch (obj) {
case Object o -> "Any object";
case String s -> "String"; // ❌ unreachable
}
Object o matches all non-null values, so String s is never reached.
8.2. With Guards
switch (obj) {
case String s when s.isEmpty() -> "Empty string";
case String s -> "Non-empty string";
}
Valid, because:
- First case matches only empty strings.
- Second matches non-empty strings.
But this is invalid:
switch (obj) {
case String s -> "Any string";
case String s when s.isEmpty() -> "Empty"; // ❌ unreachable
}
The guard does not make it more specific than the unguarded version if the unguarded version appears first.
9. Null Handling in Pattern Matching switch
9.1. Explicit null Case
switch (obj) {
case null -> "Null";
case String s -> "String: " + s;
default -> "Other";
}
9.2. NPE When Null Is Not Handled
If obj might be null and you don’t handle case null, then:
switch (obj) {
case String s -> "String"; // obj == null → NullPointerException
default -> "Other";
}
To be safe, always consider null when you don’t control the origin of the selector.
10. Using var in Patterns
You can use var in record patterns to avoid repeating types:
record Box<T>(T value) {}
switch (obj) {
case Box(var v) -> "Box of " + v;
default -> "Not a box";
}
Or mixed:
record Pair<A, B>(A first, B second) {}
switch (p) {
case Pair(String s, var second) -> "String + " + second;
case Pair(var first, var second) -> first + " & " + second;
default -> "Unknown";
}
var means “infer the type from the component type”.
11. Pattern Variable Scope & Flow
Pattern variables (like s, x, y) are in scope:
- Within their corresponding case body (
->expression or block) - Within the guard expression (
when ...)
They are not in scope:
- Outside the
switch - In previous or later cases
- In other branches
Example:
switch (obj) {
case String s when s.length() > 5 -> System.out.println("Long: " + s);
case String s -> System.out.println("Short: " + s);
default -> System.out.println("Not string");
}
// 's' is NOT visible here
12. Limitations & Edge Cases
12.1. Patterns Must Be Compatible with the Selector Type
Number n = ...;
switch (n) {
case String s -> ... // ❌ compile error (String not compatible with Number)
}
12.2. Guard Expression Must Be Well-Typed
case String s when s.length() > 0 -> ... // ✅
case String s when s = "x" -> ... // ❌ invalid: not boolean
12.3. No && Guard in Case Label
This is invalid:
case String s && s.length() > 5 -> ... // ❌
Use when instead.
13. Best Practices
✔ Prefer switch expressions for value-returning logic
String label = switch (status) {
case 200 -> "OK";
case 404 -> "Not Found";
default -> "Other";
};
✔ Use sealed types + record patterns for ADTs
sealed interface Expr permits Num, Add, Mul {}
record Num(int value) implements Expr {}
record Add(Expr left, Expr right) implements Expr {}
record Mul(Expr left, Expr right) 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);
};
}
✔ Use guarded patterns instead of nested ifs
case String s when !s.isBlank() && s.length() > 3 -> ...
instead of:
case String s -> {
if (!s.isBlank() && s.length() > 3) { ... }
}
✔ Handle null explicitly when possible
case null -> ...
❌ Avoid overly complex nested patterns and guards
Prefer readable patterns and extract complex logic into methods.
14. Typical Interview Questions & Answers
Q1. What is pattern matching for switch?
A Java feature (finalized in Java 21) that allows switch to use patterns (type, record, constants, null) instead of only primitive and enum constants, with exhaustiveness checking and pattern variables.
Q2. How do you write a guarded pattern in switch?
case Pattern p when condition -> ...
Example:
case String s when s.length() > 5 -> ...
Q3. Is case String s && s.length() > 5 -> valid?
No. That is invalid syntax. Guards in switch must use when.
Q4. How does pattern matching for switch interact with sealed types?
It allows exhaustive switches without a default, because the compiler knows all possible subtypes and can enforce that you handle all of them.
Q5. How is switch null handling changed by pattern matching?
switch now supports case null, and will throw NPE only if the selector is null and no null case is provided.
Q6. What are record patterns?
Patterns that destructure records:
case Point(int x, int y) -> ...
You can nest them and use them with when guards.
Q7. How is this different from instanceof pattern matching?
instanceofpattern matching is used inifconditions (obj instanceof String s).switchpattern matching applies patterns across multiple cases with exhaustiveness checks and supports more pattern forms (includingnull, record patterns, etc.).
15. Summary
Pattern matching for switch in Java (Java 21+):
- Brings powerful pattern-based branching to
switch - Supports:
- Type patterns
- Record patterns
- Constant patterns
- Null pattern
- Guarded patterns with
when - Works best with:
- Records
- Sealed classes
- Record patterns
- Enables expressive, concise, and type-safe control flow.
This guide is designed to be your one-stop reference for interviews and real-world development using pattern matching for switch in modern Java.