You are here
Home > java > Core Java >

Guarded Pattern in Java 21 switch-case

Guarded Pattern in Java: A Comprehensive Guide with Examples

Guarded Pattern in Java 21Java has undergone significant evolution over the years, and with the introduction of Java 17, several advanced features were added to the language. One such feature is the “Guarded Pattern” in enhanced switch expressions and pattern matching. This article explores the guarded pattern, its use cases, and practical examples to demonstrate its power and utility.

The Guarded Pattern was introduced as a preview feature in Java 17 under JEP 406 for adding conditional logic directly in switch case labels. It was finalized in Java 21, making it a stable feature for developers.

What is a Guarded Pattern?

A guarded pattern in Java is a conditional pattern that allows us to define additional constraints on pattern matching. We can create highly specific case labels in switch expressions by combining a type pattern with logical conditions (guards). This feature enhances the expressiveness of switch statements and reduces the need for traditional nested if-else conditions.

Why Use Guarded Patterns?

  • Expressiveness: Makes code concise and easier to read.
  • Specificity: Matches cases based on both type and condition.
  • Versatility: Supports complex logic directly in case label.
  • Reduction in Boilerplate code: Avoids boilerplate code, reducing the chance of bugs.

Key Concepts of Guarded Pattern in Java

  1. Type Pattern: Matches objects based on their type.
  2. Guards: Additional conditions specified using the ‘when’ keyword.
  3. Switch Expressions: Enhanced in Java 17 to allow more expressive patterns.

Syntax of Guarded Patterns

case Type variable when (condition) -> statement;

Here, Type specifies the object type, variable is the variable bound to the matched object, and condition is the additional logic to evaluate. In other words, a guarded patten label has the form ‘p when e’, where p is a pattern and e is a boolean expression. The complete switch-case statement will look like:

switch (expression) {
   case Type variable when condition -> statement;
   // Other cases
}

When Clause, Guarded Pattern Label & Guard

We can include a Boolean expression right after a pattern label with a ‘when’ clause. This is called a guarded pattern label. The Boolean expression in the when clause is called a guard. A value matches a guarded pattern label if it matches the pattern and, if so, the guard also evaluates to true.

Guarded Pattern Example

Consider the following example:

public class GuardedPatternTest {

    private static void test(Object obj) {
        switch (obj) {
            case String s when (s.contains("J") || s.contains("j")) -> 
                             System.out.println("String '"+s+"' contains 'J or j'");
            case String s -> System.out.println("String '" +s+"' doesn't contains 'J or j'");
            default ->       System.out.println("Not a string");
        }
    }

    public static void main(String[] args) {
        test("JAVA");
        test("ABC");
        test(10);
    }
}

The first pattern label (which is a guarded pattern label) matches if obj is both a String and contains a character ‘J’ or ‘j’. The second patten label matches if obj is a String and doesn’t contain these characters.

Pattern Label Dominance in Guarded Patterns vs. Normal Patterns

Pattern dominance determines which pattern in a switch block is evaluated when multiple patterns coincide or are equally applicable. With guarded patterns, dominance depends not only on the structure of the patterns but also on their guard conditions. Normal patterns, in contrast, rely mainly on structural matching and execution order.

Dominance in Normal Patterns

In normal patterns, if two case labels can match the same input, the first applicable pattern dominates. For example, consider the below code snippet:

public static void checkValue(Object obj) {
   switch (obj) {
      case Number n -> System.out.println("The number is: " + n);
      case Integer i -> System.out.println("The Integer is: " + i); // Dominated
      default -> System.out.println("Not a Number");
   }
}

Compilation error at ‘case Integer i’ : This case label is dominated by one of the preceding case labels.

If obj is a Number, it matches the case Number and bypasses case Integer, making case Integer dominated. The first pattern label ‘case Number n’ dominates the second pattern label ‘case Integer i’ because every value that matches the pattern ‘Integer i’ also matches the pattern ‘Number n’ but not the other way around. It’s because Integer is a subtype of Number.

Dominance in Guarded Patterns

Guarded patterns introduce guard conditions that refine matching behavior. A guarded pattern with a satisfied condition can dominate other patterns, even if structurally equal or earlier in the order. To overcome from the compilation error in the above example, we can add a condition using ‘when’ clause as shown in the refined example below:

public static void checkValue(Object obj) {
   switch (obj) {
      case Number n when n.intValue() > 10 -> System.out.println("The number is: " + n);
      case Integer i -> System.out.println("The Integer is: " + i); 
      default -> System.out.println("Not a Number");
   }
}

In this way, Guarded patterns enable finer-grained control by introducing conditional logic to refine matching, reducing unintended dominance in coinciding cases.

Dominance for Constant Labels in Guarded Patterns vs. Normal Pattern

A normal pattern label can dominate a constant label. For example, below code snippet can cause compile-time errors:

enum Shape { CIRCLE, SQUARE, RECTANGLE, TRIANGLE; }

public static void identifyShape(Shape shape) {
   switch (shape) {
      case Shape s -> System.out.println("General shape: " + s);
      case CIRCLE -> System.out.println("Specific shape: CIRCLE");
   }
}

In this example, the ‘case Shape s’ pattern matches all possible Shape values (e.g., CIRCLE, SQUARE, etc.). ‘case CIRCLE’ is dominated by the preceding ‘case Shape s’, that makes it unreachable.

Now let’s observe the below refined example:

enum Shape { CIRCLE, SQUARE, RECTANGLE, TRIANGLE; }

public static void identifyShape(Shape shape) {
   switch (shape) {
      case Shape s when s != Shape.CIRCLE -> System.out.println("General shape: " + s);
      case CIRCLE -> System.out.println("Specific shape: CIRCLE");
   }
}

public static void main(String[] args) {
   identifyShape(Shape.CIRCLE); 
   identifyShape(Shape.SQUARE); 
   identifyShape(Shape.TRIANGLE); 
}

Output: 

Specific shape: CIRCLE
General shape: SQUARE
General shape: TRIANGLE

The refined version uses ‘case Shape s when s != Shape.CIRCLE’, which explicitly excludes CIRCLE from matching this case. The specific case for CIRCLE is checked first. The guarded pattern handles all remaining Shape values that are not CIRCLE, that ensures no coinciding cases and resolves the dominance issue.

Scope of Pattern Variables in Guarded Patterns

In guarded patterns, the scope of a pattern variable depends on whether the guard condition evaluates to true. Pattern variables in guarded patterns are scoped tightly to their matching case or condition. In other words, the variable is usable only if the guard condition is satisfied. Like other variables, pattern variables are also only accessible within the case block or conditional expression when the pattern matches and the guard condition holds. Pattern variables are not accessible outside their matching case. Consider the following example:

record Employee(String name, int salary) {}

static void analyze(Object obj) {
    switch (obj) {
        case Employee(String name, int salary) when salary > 100000 ->
           System.out.println("High earner: " + name);
        case Employee(String name, int salary) when salary <= 100000 ->
           System.out.println("Regular employee: " + name);
        default -> System.out.println("Not an employee");
    }
      // `name` and `salary` are not accessible here.
}

In the example above, variables name and salary exist only in the case blocks where the pattern matches and the guard condition is satisfied.

Real-World Examples

Example#1: Filtering Employees Based on Role and Condition

Let’s consider an application that categorizes employees based on their roles and specific conditions:

sealed class Employee permits Manager, Developer, Intern {}

final class Manager extends Employee {
    private final int teamSize;
    public Manager(int teamSize) { this.teamSize = teamSize; }
    public int getTeamSize() { return teamSize; }
}

final class Developer extends Employee {
    private final String language;
    public Developer(String language) { this.language = language; }
    public String getLanguage() { return language; }
}

final class Intern extends Employee {}

public class GuardedPatternExample {
    
    public static String categorizeEmployee(Employee employee) {
        return switch (employee) {
            case Manager m when (m.getTeamSize() > 10) -> "Senior Manager";
            case Manager m -> "Manager";
            case Developer d when ("Java".equals(d.getLanguage())) -> "Java Developer";
            case Developer d -> "Developer";
            case Intern i -> "Intern";
            default -> "Unknown Employee";
        };
    }

    public static void main(String[] args) {
        System.out.println(categorizeEmployee(new Manager(15))); // Output: Senior Manager
        System.out.println(categorizeEmployee(new Developer("Java"))); // Output: Java Developer
        System.out.println(categorizeEmployee(new Developer("Python"))); // Output: Developer
    }
}

Example#2: Processing Orders Based on Type and Value

This example demonstrates a guarded pattern to classify orders:

sealed class Order permits BulkOrder, RetailOrder {}

final class BulkOrder extends Order {
    private final int quantity;
    public BulkOrder(int quantity) { this.quantity = quantity; }
    public int getQuantity() { return quantity; }
}

final class RetailOrder extends Order {
    private final double price;
    public RetailOrder(double price) { this.price = price; }
    public double getPrice() { return price; }
}

public class OrderProcessor {
    
    public static String processOrder(Order order) {
        return switch (order) {
            case BulkOrder b when (b.getQuantity() > 100) -> "Large Bulk Order";
            case BulkOrder b -> "Small Bulk Order";
            case RetailOrder r when (r.getPrice() > 500) -> "High-Value Retail Order";
            case RetailOrder r -> "Standard Retail Order";
            default -> "Unknown Order Type";
        };
    }

    public static void main(String[] args) {
        System.out.println(processOrder(new BulkOrder(150))); // Output: Large Bulk Order
        System.out.println(processOrder(new RetailOrder(600.0))); // Output: High-Value Retail Order
    }
}

Example#3: Validating Shapes in a Drawing Application

sealed interface Shape permits Circle, Rectangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double length, double width) implements Shape {}

public class ShapeValidator {
    
    public static String validateShape(Shape shape) {
        return switch (shape) {
            case Circle c when (c.radius() > 0) -> "Valid Circle";
            case Rectangle r when (r.length() > 0 && r.width() > 0) -> "Valid Rectangle";
            default -> "Invalid Shape";
        };
    }

    public static void main(String[] args) {
        System.out.println(validateShape(new Circle(5.0))); // Output: Valid Circle
        System.out.println(validateShape(new Rectangle(10.0, 5.0))); // Output: Valid Rectangle
        System.out.println(validateShape(new Circle(-1.0))); // Output: Invalid Shape
    }
}

Conclusion

Guarded patterns in Java 21 play an important role in the language’s expressiveness and usability. By enabling concise and powerful logic within switch expressions, they address real-world scenarios with clarity and efficiency. As demonstrated by the examples above, this feature is particularly useful for applications involving hierarchical data, complex conditions, or polymorphic types.


For other features explanation with examples on pattern matching for switch starting from Java 12 to Java 21, kindly visit complete details on Pattern Matching for Switch.

To practice coding interview questions on Java 17, kindly visit Java 17 Coding Interview Questions & Solutions.

For practical practice on Java 17 features interview questions in form of Quizzes/MVQs, kindly visit Java 17 Practice Test.


References:

https://docs.oracle.com/en/java/javase/21/language/pattern-matching-switch.html

https://openjdk.org/jeps/441

https://openjdk.org/jeps/406

Leave a Reply


Top