Back to Blog
Java

Java Guarded Patterns: Matching with Conditions

java guarded patterns: Learn how to use Java guarded patterns with the `when` clause to add conditions to pattern matching in switch expressions and statements.

Pattern MatchingSwitch ExpressionsJava 21RecordsType Patterns
Illustration of Java guarded patterns showing a switch statement with a when clause filtering values.

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 patterns.

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 pattern itself, making the code more declarative and concise.

What Are Guarded Patterns in Java?

A guarded pattern is a pattern combined with a boolean expression using the when keyword. The pattern 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 and became final in Java 21. It works in both switch statements and switch expressions.

The syntax is straightforward:

case TypePattern when condition -> ...

The condition can reference variables bound by the pattern. If the pattern does not bind any variables, the condition is still evaluated against the selector expression.

Basic Syntax: Using when in Switch Patterns

Here is a minimal example that uses a guarded pattern 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 guarded pattern 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. Guarded patterns can be applied at any level. 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 that is too broad can shadow later cases. 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 case would always match, making the guarded case dead code. The compiler will not always warn about this, so you must keep the order 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:

case String s when s.length() > 0 -> ...

If s is null, s.length() throws a NullPointerException. The switch does not catch it; it simply fails. Ensure your guards are null-safe or handle null earlier in the pattern.

Null Values

A null value does not match any type pattern unless you have an explicit case null. Guarded patterns do not change this. If you want to handle null with a condition, you must use a case null first.

Runtime Behavior and Performance

Guarded patterns do not introduce runtime overhead compared to manual if checks. The compiler translates the pattern matching into a sequence of instanceof checks and conditional branches. The when clause becomes a simple boolean evaluation in the generated bytecode. There is no reflection or dynamic dispatch involved.

This means you can use guarded patterns liberally without worrying about performance degradation. The main cost is the same as writing the equivalent if logic manually. In fact, the JIT compiler may optimize the pattern matching better because the structure is more explicit.

One subtlety: the order of evaluation is deterministic and matches the source order. This is important when guards have side effects, though side effects in guards are discouraged because they make the code harder to reason about.

Compatibility and Java Version Requirements

Guarded patterns are a final feature in Java 21. They were available as a preview in Java 17, 18, 19, and 20, but the syntax and semantics were refined. 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. The compiler will guide you if a pattern is not exhaustive or if a guard is unreachable. The refactoring is straightforward: replace the if condition inside a case with a when clause on the pattern.

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 if statements 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.

java guarded patterns: Practical Usage and Code Examples | RYUSLOG DEV