Java Guarded Patterns: Matching with Conditions
Use the when clause in Java switch pattern matching to add conditions to type and record patterns. Includes syntax, examples, and common pitfalls.
Pattern matching in Java has evolved from simple type checks to full destructuring with records. A common requirement is to match a value not only by its type or shape but also by a condition on its contents. Java guarded patterns address this with the when clause in switch pattern labels.
Consider a typical scenario where you need to process an object based on its type and a property value. Without guarded patterns, you would write a chain of if statements inside each case. With guarded patterns, the condition is part of the case label itself, making the code more declarative and concise.
What Are Guarded Patterns in Java?
A guarded case label combines a pattern with a boolean expression using the when keyword. It matches only if the value matches the underlying pattern and the condition evaluates to true. This feature was introduced as a preview in Java 17 as part of pattern matching for switch, and became final in Java 21. It works in both switch statements and switch expressions.
The syntax is straightforward:
case TypePattern when condition -> ...
The guard is a boolean expression and can reference variables bound by the pattern, as well as other variables in scope. The guard is evaluated only after the underlying pattern matches. If it evaluates to false, pattern matching continues with the next case label.
Basic Syntax: Using when in Switch Patterns
Here is a minimal example that uses a guarded case with a type pattern:
Object value = getValue(); switch (value) { case Integer i when i > 0 -> System.out.println("Positive integer: " + i); case Integer i -> System.out.println("Non-positive integer: " + i); case String s when s.length() > 5 -> System.out.println("Long string: " + s); case String s -> System.out.println("Short string: " + s); default -> System.out.println("Unknown type"); }
The first case matches only when value is an Integer and its value is greater than zero. If the value is an Integer but not greater than zero, the second case matches. The order matters: Java evaluates cases from top to bottom, and the first matching case wins. This allows you to express conditional logic without nested if statements.
Combining Guarded Patterns with Record Patterns
Record patterns destructure records into their components. Guarded patterns become especially useful when you need to validate the component values during destructuring. For example, consider a Point record:
record Point(int x, int y) {}
You can match a Point and check its coordinates in one step:
Object obj = getPoint(); switch (obj) { case Point(int x, int y) when x == y -> System.out.println("Diagonal point"); case Point(int x, int y) when x > 0 && y > 0 -> System.out.println("First quadrant"); case Point(int x, int y) -> System.out.println("Other point"); default -> System.out.println("Not a point"); }
The when clause can use any expression that references the bound variables x and y. This keeps the validation logic close to the destructuring, making the intent clear.
Guarded Patterns with Nested Patterns
Nested patterns allow you to destructure records within records. The when clause remains on the case label, and it can reference variables bound anywhere in that label's pattern. For example, suppose you have an Order record that contains a Customer record:
record Customer(String name, int loyaltyPoints) {} record Order(Customer customer, double total) {}
You can match an order from a loyal customer with a high total:
Order order = getOrder(); switch (order) { case Order(Customer c, double total) when c.loyaltyPoints > 100 && total > 500 -> System.out.println("Priority order"); case Order(Customer c, double total) when total > 100 -> System.out.println("Regular order"); default -> System.out.println("Other order"); }
The pattern binds c and total, and the guard uses both. This eliminates the need for explicit casts and nested if checks.
Common Pitfalls and Edge Cases
Guarded patterns are powerful, but they come with subtle behaviors you need to understand.
Order Sensitivity
Because guarded patterns are evaluated in order, a guarded case must appear before an unguarded case with the same type if you need the guard to be tested. For example:
switch (value) { case Integer i when i > 0 -> ... case Integer i -> ... }
The second case is reachable only when the first guard fails. If you reverse the order, the unguarded Integer case would match every Integer, making the guarded case unreachable. The compiler reports this as a compile-time error because the guarded pattern is dominated by the preceding unguarded pattern, so keep the ordering intentional.
Guard Exceptions
If the when condition throws an exception, the exception propagates out of the switch. There is no fallback to the next case. For example:
switch (value) { case String s when s.charAt(0) == 'A' -> System.out.println("Starts with A: " + s); case String s -> System.out.println("Other string: " + s); default -> System.out.println("Not a string"); }
If value is the empty string, the first case matches the String pattern but s.charAt(0) throws StringIndexOutOfBoundsException. The exception propagates to the caller; the switch does not catch it, and the second String case is not tried. Make guards safe for edge values, or test those values before relying on them.
Null Values
A null value does not match any type pattern or record pattern. It matches only an explicit case null; if none exists, switching on a null selector throws NullPointerException. Guarded patterns do not change this. If you want to handle null, add case null before the guarded cases and branch inside it.
Runtime Behavior and Performance
Guarded patterns do not introduce special-purpose runtime machinery. The compiler lowers the pattern match to ordinary instanceof checks and conditional branches, and the guard becomes a boolean expression in that flow. There is no reflection or dynamic dispatch involved, so the runtime cost is comparable to hand-written if logic.
One subtlety: the order of guard evaluation is deterministic and follows source order. This matters if guards have side effects, but side effects in guards are discouraged because they make the code harder to reason about.
Compatibility and Java Version Requirements
Pattern matching for switch, including guarded case labels, is final in Java 21. The syntax was refined over previews in Java 17, 18, 19, and 20. If you are using a preview version, you must enable preview features with --enable-preview. For production code, Java 21 or later is recommended.
When migrating older code that uses switch with if chains, you can incrementally introduce guarded patterns. Split a case whose body starts with an if into a guarded case for the true branch and an unguarded case for the fallback, and keep unguarded cases after guarded cases for the same type. The compiler reports dominated case labels, which helps you catch ordering mistakes as you refactor.
Choosing Between Guarded Patterns and Traditional if-else Chains
Guarded patterns are not always the best choice. They shine when you have a hierarchy of types and need to dispatch based on both type and value. For a simple boolean check on a single variable, a traditional if statement may be clearer.
Use guarded patterns when:
- You are already using pattern matching for type checks.
- The condition is tightly coupled to the destructured data.
- You want to avoid nested
ifstatements inside switch cases.
Avoid them when:
- The condition is complex and unrelated to the pattern's structure.
- You need to perform multiple independent checks that do not fit a linear order.
- You are targeting a Java version older than 17 without preview support.
The key is readability. If a guarded pattern makes the code more declarative and less nested, it is likely a good fit. If it forces you to contort the condition into a single expression, a traditional approach may be better.
A final consideration: guarded patterns compose well with exhaustive switches over sealed hierarchies. When you use sealed interfaces, the compiler can verify that all cases are covered, and guarded patterns allow you to refine each case without breaking exhaustiveness. This combination gives you both safety and expressiveness, which is why guarded patterns are a cornerstone of modern Java pattern matching.