Pattern Matching for switch Expressions and Statements

A switch statement transfers control to one of several statements or expressions, depending on the value of its selector expression. In earlier releases, the selector expression must evaluate to a number, string or enum constant, and case labels must be constants. However, in this release, the selector expression can be of any type, and case labels can have patterns. Consequently, a switch statement or expression can test whether its selector expression matches a pattern, which offers more flexibility and expressiveness compared to testing whether its selector expression is exactly equal to a constant.

Note:

This is a preview feature. A preview feature is a feature whose design, specification, and implementation are complete, but is not permanent. A preview feature may exist in a different form or not at all in future Java SE releases. To compile and run code that contains preview features, you must specify additional command-line options. See Preview Language and VM Features.

For background information about pattern matching for switch expressions and statements, see JEP 433.

Consider the following code that calculates the perimeter of certain shapes from the section Pattern Matching for the instanceof Operator:

interface Shape { }
record Rectangle(double length, double width) implements Shape { }
record Circle(double radius) implements Shape { }
...
    public static double getPerimeter(Shape s) throws IllegalArgumentException {
        if (s instanceof Rectangle r) {
            return 2 * r.length() + 2 * r.width();
        } else if (s instanceof Circle c) {
            return 2 * c.radius() * Math.PI;
        } else {
            throw new IllegalArgumentException("Unrecognized shape");
        }
    }

You can rewrite this code to use a pattern switch expression as follows:

    public static double getPerimeter(Shape s) throws IllegalArgumentException {
        return switch (s) {
            case Rectangle r -> 2 * r.length() + 2 * r.width();
            case Circle c    -> 2 * c.radius() * Math.PI;
            default          -> throw new IllegalArgumentException("Unrecognized shape");
        };
    }

The following example uses a switch statement instead of a switch expression:

    public static double getPerimeter(Shape s) throws IllegalArgumentException {
        switch (s) {
            case Rectangle r: return 2 * r.length() + 2 * r.width();
            case Circle c:    return 2 * c.radius() * Math.PI;
            default:          throw new IllegalArgumentException("Unrecognized shape");
        }
    }

Selector Expression Type

The type of a selector expression can either be an integral primitive type or any reference type, such as in the previous examples. The following switch expression matches the selector expression obj with type patterns that involve a class type, an enum type, a record type, and an array type:
record Point(int x, int y) { }
enum Color { RED, GREEN, BLUE; }
...
    static void typeTester(Object obj) {
        switch (obj) {
            case null     -> System.out.println("null");
            case String s -> System.out.println("String");
            case Color c  -> System.out.println("Color with " + c.values().length + " values");
            case Point p  -> System.out.println("Record class: " + p.toString());
            case int[] ia -> System.out.println("Array of int values of length" + ia.length);
            default       -> System.out.println("Something else");
        }
    }

When Clauses

A when clause enables a pattern to be refined with a Boolean expression. A pattern label that contains a when clause is called a guarded pattern label, and the Boolean expression in the when clause is called a guard. A value matches a guarded pattern label if it matches the pattern and the guard evaluates to true. Consider the following example:

    static void test(Object obj) {
        switch (obj) {
            case String s:
                if (s.length() == 1) {
                    System.out.println("Short: " + s);
                } else {
                    System.out.println(s);
                }
                break;
            default:
                System.out.println("Not a string");
        }
    }

You can move the Boolean expression s.length == 1 into the case label with a when clause:

    static void test(Object obj) {
        switch (obj) {
            case String s when s.length() == 1 -> System.out.println("Short: " + s);
            case String s                      -> System.out.println(s);
            default                            -> System.out.println("Not a string");
        }
    }

The first pattern label (which is a guarded pattern label) matches if obj is both a String and of length 1. The second patten label matches if obj is a String of a different length.

A guarded patten label has the form p when e where p is a pattern and e is a Boolean expression. The scope of any pattern variable declared in p includes e.

Pattern Label Dominance

It's possible that many pattern labels could match the value of the selector expression. To help predictability, the labels are tested in the order that they appear in the switch block. In addition, the compiler raises an error if a pattern label can never match because a preceding one will always match first. The following example results in a compile-time error:
    static void error(Object obj) {
        switch(obj) {
            case CharSequence cs ->
                System.out.println("A sequence of length " + cs.length());
            case String s -> // error: this case label is dominated by a preceding case label
                System.out.println("A string: " + s);
            default ->
                throw new IllegalStateException("Invalid argument");  
        }
    }

The first pattern label case CharSequence cs dominates the second pattern label case String s because every value that matches the pattern String s also matches the pattern CharSequence cs but not the other way around. It's because String is a subtype of CharSequence.

A pattern label can dominate a constant label. These examples cause compile-time errors:

    static void error2(Integer value) {
        switch(value) {
            case Integer i ->
                System.out.println("Integer: " + i);
            case -1, 1 -> // Compile-time errors for both cases -1 and 1:
                          // this case label is dominated by a preceding case label       
                System.out.println("The number 42");
            default ->
                throw new IllegalStateException("Invalid argument");
        }
    }
    
    enum Color { RED, GREEN, BLUE; }
    
    static void error3(Color value) {
        switch(value) {
            case Color c ->
                System.out.println("Color: " + c);
            case RED -> // error: this case label is dominated by a preceding case label
                System.out.println("The color red");
        }        
    }

A guarded pattern label (see When Clauses) can also dominate a constant label:

    static void error4(Integer value) {
        switch(value) {
            case Integer i when i > 0 ->
                System.out.println("Positive integer");
            case -1, 1 -> // Compile-time errors for both cases -1 and 1:
                          // this case label is dominated by a preceding case label
                System.out.println("Value is 1 or -1");
            default ->
                throw new IllegalStateException("Invalid argument");
        }
    }    

Even though the guarded pattern label case Integer i when i > 0 doesn't match the value -1, the compiler still generates an error.

To resolve these compiler errors related to dominance, ensure that constant labels appear before guarded pattern labels, which must appear before non-guarded type pattern labels:

    static void checkIntegers(Integer value) {
        switch(value) {
            case -1, 1 -> // Constant labels
                System.out.println("Value is 1 or -1");
            case Integer i when i > 0 -> // Guarded pattern label
                System.out.println("Positive integer");
            case Integer i -> // Non-guarded type pattern label
                System.out.println("Neither positive, 1, nor -1");
        }
    }

Type Coverage in switch Expressions and Statements

As described in Switch Expressions, the switch blocks of switch expressions and switch statements, which use pattern or null labels, must be exhaustive. This means that for all possible values, there must be a matching switch label. The following switch expression is not exhaustive and generates a compile-time error. Its type coverage consists of the subtypes of String or Integer, which doesn't include the type of the selector expression, Object:

    static int coverage(Object obj) {
        return switch (obj) {         // Error - not exhaustive
            case String s  -> s.length();
            case Integer i -> i;
        };
    }

However, the type coverage of the case label default is all types, so the following example compiles:

    static int coverage(Object obj) {
        return switch (obj) { 
            case String s  -> s.length();
            case Integer i -> i;
            default        -> 0;
        };
    }

The compiler takes into account whether the type of a selector expression is a sealed class. The following switch expression compiles. It doesn't need a default case label because its type coverage is the classes A, B, and C, which are the only permitted subclasses of S, the type of the selector expression:

sealed interface S permits A, B, C { }
final class A implements S { }
final class B implements S { }
record C(int i) implements S { }  // Implicitly final
...
    static int testSealedCoverage(S s) {
        return switch (s) {
            case A a -> 1;
            case B b -> 2;
            case C c -> 3;
        };
    }

The compiler can also determine the type coverage of a switch expression or statement if the type of its selector expression is a generic sealed class. The following example compiles. The only permitted subclasses of interface I are classes A and B. However, because the selector expression is of type I<Integer>, the switch block requires only class B in its type coverage to be exhaustive:

    sealed interface I<T> permits A, B {}
    final class A<X> implements I<String> {}
    final class B<Y> implements I<Y> {}

    static int testGenericSealedExhaustive(I<Integer> i) {
        return switch (i) {
        // Exhaustive as no A case possible!  
            case B<Integer> bi -> 42;
        };
    }

The type of a switch expression or statement's selector expression can also be a generic record. As always, a switch expression or statement must be exhaustive. The following example doesn't compile. No match for a Pair exists that contains two values, both of type A:

record Pair<T>(T x, T y) {}
class A {}
class B extends A {}

    static void notExhaustive(Pair<A> p) {
        switch (p) {                 
            // error: the switch statement does not cover all possible input values
            case Pair<A>(A a, B b) -> System.out.println("Pair<A>(A a, B b)");
            case Pair<A>(B b, A a) -> System.out.println("Pair<A>(B b, A a)");
        }        
    }

The following example compiles. Interface I is sealed. Types C and D cover all possible instances:

record Pair<T>(T x, T y) {}
sealed interface I permits C, D {}
record C(String s) implements I {}
record D(String s) implements I {}

    static void exhaustiveSwitch(Pair<I> p) {
        switch (p) {
            case Pair<I>(I i, C c) -> System.out.println("C = " + c.s());
            case Pair<I>(I i, D d) -> System.out.println("D = " + d.s());
        }
    }

If a switch expression or statement is exhaustive at compile time but not at run time, then a MatchException is thrown. This can happen when a class that contains an exhaustive switch expression or statement has been compiled, but a sealed hierarchy that is used in the analysis of the switch expression or statement has been subsequently changed and recompiled. Such changes are migration incompatible and may lead to a MatchException being thrown when running the switch statement or expression. Consequently, you need to recompile the class containing the switch expression or statement.

Consider the following two classes ME and Seal:

class ME {
    public static void main(String[] args) {
        System.out.println(switch (Seal.getAValue()) {
            case A a -> 1;
            case B b -> 2;
        });
    }
}
sealed interface Seal permits A, B {
    static Seal getAValue() {
        return new A();
    }
}
final class A implements Seal {}
final class B implements Seal {}

The switch expression in the class ME is exhaustive and this example compiles. When you run ME, it prints the value 1. However, suppose you edit Seal as follows and compile this class and not ME:

sealed interface Seal permits A, B, C {
    static Seal getAValue() {
        return new A();
    }
}
final class A implements Seal {}
final class B implements Seal {}
final class C implements Seal {}

When you run ME, it throws a MatchException:

Exception in thread "main" java.lang.MatchException
            at ME.main(ME.java:3)

Inference of Type Arguments in Record Patterns

The compiler can infer the type arguments for a generic record pattern in all constructs that accept patterns: switch statements, instanceof expressions, and enhanced for statements.

In the following example, the compiler infers MyPair(var s, var i) as MyPair<String, Integer>(String s, Integer i):

record MyPair<T, U>(T x, U y) { }

   static void recordInference(MyPair<String, Integer> p){
        switch (p) {
            case MyPair(var s, var i) -> 
                System.out.println(s + ", #" + i);
        }
    }   

See Record Patterns for more examples of inference of type arguments in record patterns.

Scope of Pattern Variable Declarations

As described in the section Pattern Matching for the instanceof Operator, the scope of a pattern variable is the places where the program can reach only if the instanceof operator is true:

    public static double getPerimeter(Shape shape) throws IllegalArgumentException {
        if (shape instanceof Rectangle s) {
            // You can use the pattern variable s of type Rectangle here.
        } else if (shape instanceof Circle s) {
            // You can use the pattern variable s of type Circle here
            // but not the pattern variable s of type Rectangle.
        } else {
            // You cannot use either pattern variable here.
        }
    }

More specifically, the scope of a pattern variable declared in a case label includes the following:

  • The when clause of the case label:
    static void test(Object obj) {
        switch (obj) {
            case Character c when c.charValue() == 7:
                System.out.println("Ding!");
                break;
            default:
                break;
            }
        }
    }

    The scope of pattern variable c includes the when clause of the case label that contains the declaration of c.

  • The expression, block, or throw statement that appears to the right of the arrow of the case label:

        static void test(Object obj) {
            switch (obj) {
                case Character c -> {
                    if (c.charValue() == 7) {
                        System.out.println("Ding!");
                    }
                    System.out.println("Character, value " + c.charValue());
                }
                case Integer i ->
                    System.out.println("Integer: " + i);  
                default -> {
                    break; 
                }
            }
        }

    The scope of pattern variable c includes the block to the right of case Character c ->. The scope of pattern variable i includes the println statement to the right of case Integer i ->.

  • The switch-labeled statement group of a case label:

        static void test(Object obj) {
            switch (obj) {
                case Character c:
                    if (c.charValue() == 7) {
                        System.out.print("Ding ");
                    }
                    if (c.charValue() == 9) {
                        System.out.print("Tab ");
                    }
                    System.out.println("character, value " + c.charValue());
                default:
                    // You cannot use the pattern variable c here:
                    break;
            }
        }

    The scope of pattern variable c includes the case Character c statement group: the two if statements and the println statement that follows them. The scope doesn't include the default statement group even though the switch statement can execute the case Character c statement group, fall through the default label, and then execute the default statement group.

    Note:

    You will get a compile-time error if it's possible to fall through a case label that declares a pattern variable. The following example doesn't compile:
        static void test(Object obj) {
            switch (obj) {
                case Character c:
                    if (c.charValue() == 7) {
                        System.out.print("Ding ");
                    }
                    if (c.charValue() == 9) {
                        System.out.print("Tab ");
                    }
                    System.out.println("character");
                case Integer i:                 // Compile-time error
                    System.out.println("An integer " + i);
                default:
                    System.out.println("Neither character nor integer");
            }
        }

    If this code were allowed, and the value of the selector expression, obj, were a Character, then the switch statement can execute the case Character c statement group and then fall through the case Integer i label, where the pattern variable i would have not been initialized.

Null case Labels

Prior to this preview feature, switch expressions and switch statements threw a NullPointerException if the value of the selector expression is null. However, to add more flexibility, a null case label is available:
    static void test(Object obj) {
        switch (obj) {
            case null     -> System.out.println("null!");
            case String s -> System.out.println("String");
            default       -> System.out.println("Something else");
        }
    }

This example prints null! when obj is null instead of throwing a NullPointerException.

You can combine a null case label with another pattern label:

    static void testStringOrNull(Object obj) {
        switch (obj) {
            case null, String s -> System.out.println("String: " + s);
            default             -> System.out.println("Something else");
        }
    }  

If a selector expression evaluates to null and the switch block does not have null case label, then a NullPointerException is thrown as normal. Consider the following switch statement:

        String s = null;
        switch (s) {
            case Object obj -> System.out.println("This doesn't match null");
            // No null label; NullPointerException is thrown
            // if s is null
        }

Although the pattern label case Object obj matches objects of type String, this example throws a NullPointerException. The selector expression evaluates to null, and the switch expression doesn't contain a null case label.

Parenthesized Patterns

A parenthesized pattern is a pattern surrounded by a pair of parentheses. Because guarded patterns combine patterns and expressions, you might introduce parsing ambiguities. You can surround patterns with parentheses to avoid these ambiguities, force the compiler to parse an expression containing a pattern differently, or increase the readability of your code. Consider the following example:

    static Function<Integer, String> testParen(Object obj) {
        boolean b = true;
        return switch (obj) {
            case String s && b -> t -> s;
            default            -> t -> "Default string";
        };
    }    

This example compiles. However, if you want to make it clear that the first arrow token (->) is part of the case label and not part of a lambda expression, you can surround the guarded pattern with parentheses:

    static Function<Integer, String> testParen(Object obj) {
        boolean b = true;
        return switch (obj) {
            case (String s && b) -> t -> s;
            default              -> t -> "Default string";
        };
    }