BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles A Comprehensive Guide to Java's New Feature: Pattern Matching for Switch

A Comprehensive Guide to Java's New Feature: Pattern Matching for Switch

This item in japanese

Key Takeaways

  • Pattern matching for the switch control-flow statement is a new feature introduced in Java 17 and refined in subsequent versions.
  • A pattern can be used in case labels as case p. The selector expression is evaluated, and the resulting value is tested against the case labels that may include patterns. The execution path of the first matching case label applies to the switch statement/expression.
  • Pattern matching adds support for a selector expression of any reference type in addition to the existing legacy types.
  • Guarded patterns can be used with the new when clause in a case label pattern.
  • Pattern matching can be used with the traditional switch statements and with the traditional fall-through semantics of a switch statement.

A switch statement is a control-flow statement that was originally designed to be a short-form alternative to the if-else if-else control-flow statement for certain use cases that involved multiple possible execution paths based on what a given expression evaluates to.

A switch statement consists of a selector expression and a switch block consisting of case labels; the selector expression is evaluated, and the execution path is switched based on which case label matches the result of the evaluation.

Originally switch could only be used as a statement with the traditional case ...: label syntax with fall-through semantics. Java 14 added support for the new case ...-> label syntax with no fall-through semantics.

Java 14 also added support for switch expressions. A switch expression evaluates to a single value. A yield statement was introduced to yield a value explicitly.

Support for switch expressions, which is discussed in detail in another article, means that switch can be used in instances that expect an expression such as an assignment statement.

Problem

However, even with the enhancements in Java 14, the switch still has some limitations:

  1. The selector expression of switch supports only specific types, namely integral primitive data types byte, short, char, and int; the corresponding boxed forms Byte, Short, Character, and Integer; the String class; and the enumerated types.
  2. The result of the switch selector expression can be tested only for exact equality against constants. Matching a case label with a constant test only against one value.
  3. The null value is not handled like any other value.
  4. Error handling is not uniform.
  5. The use of enums is not well-scoped.

Solution

A convenient solution has been proposed and implemented to counter these limitations: pattern matching for switch statements and expressions. This solution addresses all the issues mentioned above.

Pattern matching for the switch was introduced in JDK 17, refined in JDK 18, 19, and 20, and is to be finalized in JDK 21.

Pattern matching overcomes the limitations of the traditional switch in several ways:

  1. The type of the selector expression can be any reference type in addition to an integral primitive type (excluding long).
  2. Case labels can include patterns in addition to constants. A pattern case label can apply to many values, unlike a constant case label that applies to only one value. A new case label, case p, is introduced in which p is a pattern.
  3. Case labels can include null.
  4. An optional when clause can follow a case label for conditional or guarded pattern matching. A case label with a when is called a guarded case label.
  5. Enum constant case labels can be qualified. The selector expression doesn’t have to be an enum type when using enum constants when using enum constants.
  6. The MatchException is introduced for a more uniform error handling in pattern matching.
  7. The traditional switch statements and the traditional fall-through semantics also support pattern matching.

A benefit of pattern matching is to facilitate data oriented programming, such as improving the performance of complex data-oriented queries.

What is pattern matching?

Pattern matching is a powerful feature that extends the functionality of control-flow structures in programming. This feature allows a selector expression to be tested against several patterns in addition to the test against traditionally supported constants. The semantics of the switch stays unchanged; a switch selector expression value is tested against case labels that may include patterns, and if the selector expression value matches a case label pattern, that case label applies to the execution path of the switch control-flow. The only enhancement is that the selector expression can be any reference type in addition to primitive integral types (excluding long). The case labels can include patterns in addition to constants. Additionally, supporting null and qualified enum constants in case labels is an added feature.

The grammar of switch labels in a switch block is as follows:

SwitchLabel:
  case CaseConstant { , CaseConstant }
  case null [, default]
  case Pattern
  default

Pattern matching can be used with the traditional case …: label syntax with fall-through semantics, and with the case … -> label syntax with no fall-through semantics. Nonetheless, it’s essential to note that a switch block cannot mix the two types of case labels.

With these modifications in place, pattern matching has paved the way for more sophisticated control-flow structures, transforming the richer way to approach logic in code.

Setting the environment

The only prerequisite to running the code samples in this article is to install Java 20 or Java 21 (if available). Java 21 makes only one enhancement over Java 20, which is support for qualified enum constants in case labels. The Java version can be found with the following command:

java --version
java version "20.0.1" 2023-04-18
Java(TM) SE Runtime Environment (build 20.0.1+9-29)
Java HotSpot(TM) 64-Bit Server VM (build 20.0.1+9-29, mixed mode, sharing)

Because switch pattern matching is a preview feature in Java 20, javac and java commands must be run with the following syntax:

javac --enable-preview --release 20 SampleClass.java
java --enable-preview  SampleClass

However, one can directly run this using the source code launcher. In that case, the command line would be:

java --source 20 --enable-preview Main.java

The jshell option is also available but requires enabling the preview feature as well:

jshell --enable-preview

A simple example of pattern matching

We start with a simple example of pattern matching in which the selector expression type of a switch expression is reference type; Collection; and the case labels include patterns of the form case p.  

import java.util.Collection;
import java.util.LinkedList;
import java.util.Stack;
import java.util.Vector;

public class SampleClass {
    static Object get(Collection c) {

        return switch (c) {
            case Stack s -> s.pop();
            case LinkedList l -> l.getFirst();
            case Vector v -> v.lastElement();
            default -> c;
        };
    }

    public static void main(String[] argv) {

        var stack = new Stack<String>();
        stack.push("firstStackItemAdded");
        stack.push("secondStackItemAdded");
        stack.push("thirdStackItemAdded");

        var linkedList = new LinkedList<String>();

        linkedList.add("firstLinkedListElementAdded");
        linkedList.add("secondLinkedListElementAdded");
        linkedList.add("thirdLinkedListElementAdded");

        var vector = new Vector<String>();

        vector.add("firstVectorElementAdded");
        vector.add("secondVectorElementAdded");
        vector.add("thirdVectorElementAdded");

        System.out.println(get(stack));
        System.out.println(get(linkedList));
        System.out.println(get(vector));
    }
}

Compile and run the Java application, with the output:

thirdStackItemAdded
firstLinkedListElementAdded
thirdVectorElementAdded

Pattern matching supports all reference types

In the example given earlier, the Collection class type is used as the selector expression type. However, any reference type can serve as the selector expression type. Therefore, the case label patterns can be of any reference type compatible with the selector expression value. For example, the following modified SampleClass uses an Object type selector expression and includes case label patterns for a record pattern and an array reference type pattern, in addition to the case label patterns for previously used Stack, LinkedList, and Vector reference types.

import java.util.LinkedList;
import java.util.Stack;
import java.util.Vector;

record CollectionType(Stack s, Vector v, LinkedList l) {
}

public class SampleClass {
    static Object get(Object c) {
        return switch (c) {
            case CollectionType r -> r.toString();
            case String[] arr -> arr.length;
            case Stack s -> s.pop();
            case LinkedList l -> l.getFirst();
            case Vector v -> v.lastElement();
            default -> c;
        };
    }

    public static void main(String[] argv) {

        var stack = new Stack<String>();
        stack.push("firstStackItemAdded");
        stack.push("secondStackItemAdded");
        stack.push("thirdStackItemAdded");

        var linkedList = new LinkedList<String>();

        linkedList.add("firstLinkedListElementAdded");
        linkedList.add("secondLinkedListElementAdded");
        linkedList.add("thirdLinkedListElementAdded");

        var vector = new Vector<String>();

        vector.add("firstVectorElementAdded");
        vector.add("secondVectorElementAdded");
        vector.add("thirdVectorElementAdded");

        var r = new CollectionType(stack, vector, linkedList);
        System.out.println(get(r));
        String[] stringArray = {"a", "b", "c"};

        System.out.println(get(stringArray));
        System.out.println(get(stack));
        System.out.println(get(linkedList));
        System.out.println(get(vector));

    }
}

This time the output is as follows:

CollectionType[s=[firstStackItemAdded, secondStackItemAdded, thirdStackItemAdded
], v=[firstVectorElementAdded, secondVectorElementAdded, thirdVectorElementAdded
], l=[firstLinkedListElementAdded, secondLinkedListElementAdded, thirdLinkedList
ElementAdded]]
3
thirdStackItemAdded
firstLinkedListElementAdded
thirdVectorElementAdded

The null case label

Traditionally, a switch throws a NullPointerException at runtime if the selector expression evaluates to null. A null selector expression is not a compile-time issue. The following simple application with a match-all case label default demonstrates how a null selector expression throws a NullPointerException at runtime.

import java.util.Collection;

public class SampleClass {
    static Object get(Collection c) {
        return switch (c) {
            default -> c;
        };
    }

    public static void main(String[] argv) {
        get(null);
    }
}

It is possible to test a null value explicitly outside the switch block and invoke a switch only if non-null, but that involves adding if-else code. Java has added support for the case null in the new pattern matching feature. The switch statement in the following application uses the case null to test the selector expression against null.

import java.util.Collection;

public class SampleClass {
    static void get(Collection c) {

        switch (c) {
            case null -> System.out.println("Did you call the get with a null?");
            default -> System.out.println("default");
        }
    }

    public static void main(String[] argv) {
        get(null);
    }
}

At runtime, the application outputs:

Did you call the get with a null?

The case null can be combined with the default case as follows:

import java.util.Collection;

public class SampleClass {
    static void get(Collection c) {
        switch (c) {
            case null, default -> System.out.println("Did you call the get with a null?");
        }
    }

    public static void main(String[] argv) {
        get(null);
    }
}

However, the case null cannot be combined with any other case label. For example, the following class combines the case null with a case label with a pattern Stack s:

import java.util.Collection;
import java.util.Stack;

public class SampleClass {
    static void get(Collection c) {
        switch (c) {
            case null, Stack s -> System.out.println("Did you call the get with a null?");
            default -> System.out.println("default");
        }
    }

    public static void main(String[] args) {
        get(null);
    }
}

The class generates a compile-time error:

SampleClass.java:11: error: invalid case label combination
          case null, Stack s -> System.out.println("Did you call the get with a null?");

Guarded patterns with the when clause  

Sometimes, developers may use a conditional case label pattern that is matched based on the outcome of a boolean expression. This is where the when clause comes in handy. This clause evaluates a boolean expression, forming what is known as a 'guarded pattern.' For example, the when clause in the first case label in the following code snippet determines if a Stack is empty.

import java.util.Stack;
import java.util.Collection;

public class SampleClass {
    static Object get(Collection c) {
        return switch (c) {
            case Stack s when s.empty() -> s.push("first");
            case Stack s2 -> s2.push("second");
            default -> c;
        };
    }
}

The corresponding code, located to the right of the '->', only executes if the Stack is indeed empty.

The ordering of the case labels with patterns is significant

When using case labels with patterns, developers must ensure an order that doesn't create any issues related to type or subtype hierarchy. That is because, unlike constant case labels, patterns in case labels allow a selector expression to be compatible with multiple case labels containing patterns. The switch pattern matching feature matches the first case label, where the pattern matches the value of the selector expression.  

If the type of a case label pattern is a subtype of the type of another case label pattern that appears before it, a compile-time error will occur because the latter case label will be identified as unreachable code.  

To demonstrate this scenario, developers can compile and run the following sample class in which a case label pattern of type Object dominates a subsequent code label pattern of type Stack.

import java.util.Stack;

public class SampleClass {
    static Object get(Object c) {
        return switch (c) {
            case Object o  -> c;
            case Stack s  -> s.pop();
        };
    }
}

When compiling the class, an error message is produced:

SampleClass.java:12: error: this case label is dominated by a preceding case lab
el
        case Stack s  -> s.pop();
             ^

The compile-time error can be fixed simply by reversing the order of the two case labels as follows:

public class SampleClass {
    static Object get(Object c) {
        return switch (c) {
            case Stack s  -> s.pop();
            case Object o  -> c;
        };
    }
}

Similarly, if a case label includes a pattern that is of the same reference type as a preceding case label with an unconditional/unguarded pattern (guarded patterns discussed in an earlier section), it will result in a  compile-type error for the same reason, as in the class:

import java.util.Stack;
import java.util.Collection;

public class SampleClass {
    static Object get(Collection c) {
        return switch (c) {
            case Stack s -> s.push("first");
            case Stack s2 -> s2.push("second");
        };
    }
}

Upon compilation, the following error message is generated:

SampleClass.java:13: error: this case label is dominated by a preceding case lab
el
        case Stack s2 -> s2.push("second");
             ^

To avoid such errors, developers should maintain a straightforward and readable ordering of case labels. The constant labels should be listed first, followed by the case null label, the guarded pattern labels, and the non-guarded type pattern labels. The default case label can be combined with the case null label, or placed separately as the last case label. The following class demonstrates the correct ordering:

import java.util.Collection;
import java.util.Stack;
import java.util.Vector;

public class SampleClass {
    static Object get(Collection c) {
        return switch (c) {
            case null -> c;  //case label null
            case Stack s when s.empty() -> s.push("first");  // case label with guarded pattern
            case Vector v when v.size() > 2 -> v.lastElement();  // case label with guarded pattern
            case Stack s -> s.push("first");  // case label with unguarded pattern
            case Vector v -> v.firstElement();  // case label with unguarded pattern
            default -> c;
        };
    }
}

Pattern matching can be used with the traditional switch statement and with fall-through semantics

The pattern-matching feature is independent of whether it is a switch statement or a switch expression. The pattern matching is also independent of whether the fall-through semantics with case …: labels, or the no-fall-through semantics with the case …-> labels is used. In the following example, pattern matching is used with a switch statement and not a switch expression. The case labels use the fall-through semantics with the case …: labels. A when clause in the first case label uses a guarded pattern.  

import java.util.Stack;
import java.util.Collection;

public class SampleClass {
    static void get(Collection c) {
        switch (c) {
            case Stack s when s.empty(): s.push("first"); break;
            case Stack s : s.push("second");  break;
            default : break;
        }
    }
}

Scope of pattern variables

A pattern variable is a variable that appears in a case label pattern. The scope of a pattern variable is limited to the block, expression, or throw statement that appears to the right of the -> arrow. To demonstrate, consider the following code snippet in which a pattern variable from a preceding case label is used in the default case label.

import java.util.Stack;

public class SampleClass {
    static Object get(Object c) {
        return switch (c) {
            case Stack s -> s.push("first");
            default -> s.push("first");
        };
    }
}

A compile-time error results:

import java.util.Collection;
SampleClass.java:13: error: cannot find symbol
        default -> s.push("first");
                   ^
  symbol:   variable s
  location: class SampleClass

The scope of a pattern variable that appears in the pattern of a guarded case label includes the when clause, as demonstrated in the example:

import java.util.Stack;
import java.util.Collection;

public class SampleClass {
    static Object get(Collection c) {
        return switch (c) {
            case Stack s when s.empty() -> s.push("first");
            case Stack s -> s.push("second");
            default -> c;
        };
    }
}

Given the limited scope of a pattern variable, the same pattern variable name can be used across multiple case labels. This is illustrated in the preceding example, where the pattern variable s is used in two different case labels.

When dealing with a case label with fall-through semantics, the scope of a pattern variable extends to the group of statements located to the right of the ':'. That is why it was possible to use the same pattern variable name for the two case labels in the previous section by using pattern matching with the traditional switch statement. However, fall through case label that declares a pattern variable is a compile-time error. This can be demonstrated in the following variation of the earlier class:

import java.util.Stack;
import java.util.Vector;
import java.util.Collection;

public class SampleClass {
    static void get(Collection c) {
        switch (c) {
            case Stack s : s.push("second");
            case Vector v  : v.lastElement();
            default : System.out.println("default");
        }
    }
}

Without a break; statement in the first statement group, the switch could fall-through the second statement group without initializing the pattern variable v in the second statement group. The preceding class would generate a compile-time error:

SampleClass.java:12: error: illegal fall-through to a pattern
        case Vector v  : v.lastElement();
             ^

Simply adding a break; statement in the first statement group as follows fixes the error:

import java.util.Stack;
import java.util.Vector;
import java.util.Collection;

public class SampleClass {
    static void get(Collection c) {
        switch (c) {
            case Stack s : s.push("second"); break;
            case Vector v  : v.lastElement();
            default : System.out.println("default");
        }
    }
}

Only one pattern per case label

Combining multiple patterns within a single case label, whether it is a case label of the type case …:, or the type case …->  is not allowed, and it is a compile-time error. It may not be obvious, but combining patterns in a single case label incurs fall-through a pattern, as demonstrated by the following class.

import java.util.Stack;
import java.util.Vector;
import java.util.Collection;

public class SampleClass {
    static Object get(Collection c) {
        return switch (c) {
            case Stack s, Vector v -> c;
            default -> c;
        };
    }
}

A compile-time error is generated:

SampleClass.java:11: error: illegal fall-through from a pattern
        case Stack s, Vector v -> c;
                      ^

Only one match-all case label in a switch block

It is a compile-time error to have more than one match-all case labels in a switch block, whether it is a switch statement or a switch expression. The match-all case labels are :

  1. A case label with a pattern that unconditionally matches the selector expression
  2. The default case label

To demonstrate, consider the following class:

import java.util.Collection;

public class SampleClass {
    static Object get(Collection c) {
        return switch (c) {
            case Collection coll -> c;
            default -> c;
        };
    }
}

Compile the class, only to get an error message:

SampleClass.java:13: error: switch has both an unconditional pattern and a default label
        default -> c;
        ^

The exhaustiveness of type coverage

Exhaustiveness implies that a switch block must handle all possible values of the selector expression. The exhaustiveness requirement is implemented only if one or more of the following apply:

  • a) Pattern switch expressions/statements are used,
  • b) The case null is used,
  • c) The selector expression is not one of the legacy types (char, byte, short, int, Character, Byte, Short, Integer, String, or an enum type).

To implement exhaustiveness, it may suffice to add case labels for each of the subtypes of the selector expression type if the subtypes are few.  However, this approach could be tedious if subtypes are numerous; for example, adding a case label for each reference type for a selector expression of type Object, or even each of the subtypes for a selector expression of type Collection, is just not feasible.

To demonstrate the exhaustiveness requirement, consider the following class:

import java.util.Collection;
import java.util.Stack;
import java.util.LinkedList;
import java.util.Vector;

public class SampleClass {
    static Object get(Collection c)   {
        return switch (c) {
            case Stack s  -> s.push("first");
            case null  -> throw new NullPointerException("null");
            case LinkedList l    -> l.getFirst();
            case Vector v  -> v.lastElement();
        };
    }
}  


The class generates a compile-time error message:

SampleClass.java:10: error: the switch expression does not cover all possible in
put values
                return switch (c) {
                       ^

The issue can be fixed simply by adding a default case as follows:

import java.util.Collection;
import java.util.Stack;
import java.util.LinkedList;
import java.util.Vector;

public class SampleClass {
    static Object get(Collection c)   {
        return switch (c) {
            case Stack s  -> s.push("first");
            case null  -> throw new NullPointerException("null");
            case LinkedList l    -> l.getFirst();
            case Vector v  -> v.lastElement();
            default -> c;
        };
    }
}  

A match-all case label with a pattern that unconditionally matches the selector expression, such as the one in the following class, would be exhaustive, but it wouldn’t handle or process any subtypes distinctly.

import java.util.Collection;

public class SampleClass {
    static Object get(Collection c)   {
        return switch (c) {
            case Collection coll  -> c;
        };
    }
}  

The default case label could be needed for exhaustiveness but could sometimes be avoided if the possible values of a selector expression are very few. As an example, if the selector expression is of type java.util.Vector, only one case label pattern for the single subclass java.util.Stack is required to avoid the default case. Similarly, if the selector expression is a sealed class type, only the classes declared in the permits clause of the sealed class type need to be handled by the switch block.

Generics record patterns in switch case label

Java 20 adds support for an inference of type arguments for generic record patterns in switch statements/expressions. As an example, consider the generic record:

record Triangle<S,T,V>(S firstCoordinate, T secondCoordinate,V thirdCoordinate){};

In the following switch block, the inferred record pattern is

Triangle<Coordinate,Coordinate,Coordinate>(var f, var s, var t):
 
static void getPt(Triangle<Coordinate, Coordinate, Coordinate> tr){
        switch (tr) {
           case Triangle(var f, var s, var t) -> …;
           case default -> …;
        }
}

Error handling with MatchException

Java 19 introduces a new subclass of the java.lang.Runtime class for a more uniform exception handling during pattern matching. The new class called java.lang.MatchException is a preview API. The MatchException is not designed specifically for pattern matching in a switch but rather for any pattern-matching language construct. MatchException may be thrown at runtime when an exhaustive pattern matching does not match any of the provided patterns. To demonstrate this, consider the following application that includes a record pattern in a case label for a record that declares an accessor method with division by 0.

record DivisionByZero(int i) {
    public int i() {
        return i / 0;
    }
}


public class SampleClass {

    static DivisionByZero get(DivisionByZero r) {
        return switch(r) {
        case DivisionByZero(var i) -> r;
        };

    }

    public static void main(String[] argv) {

        get(new DivisionByZero(42));
    }
}

The sample application compiles without an error but at runtime throws a MatchException:

Exception in thread "main" java.lang.MatchException: java.lang.ArithmeticException: / by zero
        at SampleClass.get(SampleClass.java:7)
        at SampleClass.main(SampleClass.java:14)
Caused by: java.lang.ArithmeticException: / by zero
        at DivisionByZero.i(SampleClass.java:1)
        at SampleClass.get(SampleClass.java:1)
        ... 1 more

Conclusion

This article introduces the new pattern-matching support for the switch control-flow construct. The main improvements are that switch's selector expression can be any reference type, and the switch's case labels can include patterns, including conditional pattern matching. And, if you rather not update your complete codebase, pattern matching is supported with traditional switch statements and with traditional fall-through semantics.  

About the Author

Rate this Article

Adoption
Style

BT