(翻译中)

原文链接:JEP 406: Pattern Matching for switch (Preview)

摘要

Enhance the Java programming language with pattern matching for switch expressions and statements, along with extensions to the language of patterns. Extending pattern matching to switch allows an expression to be tested against a number of patterns, each with a specific action, so that complex data-oriented queries can be expressed concisely and safely.

目标

  • 通过允许模式出现在 case 标签中扩展 switch 表达式和语句的表达能力与适用范围。
  • 在必要时放宽 switch 对于 null 的限制。
  • 引入两种新的模式:允许使用任意布尔表达式优化模式匹配逻辑的守卫模式(Guarded Pattern),以及用于解决一些解析歧义的括号模式(Parenthesized Pattern)
  • 确保所有现有的 switch 表达式和语句依然能在不经过任何修改的情况下通过编译,并以相同的语义执行。
  • 不引入新的、独立于传统 switch 结构的、类似 switch 的模式匹配表达式或语句。
  • switch 表达式和语句的行为在 case 标签是模式与是 case 标签是传统常量时应该保持一致。

动机

在 Java 16 中,JEP 394 扩展了 instanceof 运算符,使其能够使用类型模式执行模式匹配。这种适度的扩展能够简化我们熟悉的 instanceof 与类型转换惯用法:

// Old code
if (o instanceof String) {
    String s = (String)o;
    ... use s ...
}

// New code
if (o instanceof String s) {
    ... use s ...
}

我们经常希望把 o 这样的变量与多个备选方案比较。Java 支持使用 switch 语句,以及 Java 14 引入的 switch 表达式(JEP 361)进行多路比较,但很不幸的是,switch 的局限性很大。你只能用它判断少数几个类型——数字类型、枚举类型和字符串——并且只能测试是否与常量完全相等。我们希望能够使用模式来测试同一个变量的多种可能性,对每种可能性采取特定的操作,但因为现有的 switch 不支持这一点,所以我们只能这样使用一系列 if...else 进行测试:

static String formatter(Object o) {
    String formatted = "unknown";
    if (o instanceof Integer i) {
        formatted = String.format("int %d", i);
    } else if (o instanceof Long l) {
        formatted = String.format("long %d", l);
    } else if (o instanceof Double d) {
        formatted = String.format("double %f", d);
    } else if (o instanceof String s) {
        formatted = String.format("String %s", s);
    }
    return formatted;
}

这段代码得益于使用 instanceof 模式,但依然不够完美。首先,最重要的是,因为我们使用了一种通用控制结构,所以这种方式可能掩藏编码错误。这段代码目的是在 if...else 的每个分支中为 formatted 赋值,但编译器无法识别和验证它为不变量。如果某个块(可能是很少执行的块)中没有为 formatted 赋值,这就会产生一个 bug(将 formatted 声明为空的局部变量至少能让编译器在这里进行明确赋值分析,但并不总是能这样写)。此外,上面的代码是不可优化的;如果没有编译器支持,它将具有 O(n) 的时间复杂度,即使底层问题通常是 O(1) 的。

不过 switch 完美符合模式匹配!如果我们将 swith 语句和表达式扩展到任何类型,并允许使用带有模式的而不仅仅是常量的 case 标签,那么我们可以更清晰、更可靠地重写上述代码:

static String formatterPatternSwitch(Object o) {
    return switch (o) {
        case Integer i -> String.format("int %d", i);
        case Long l    -> String.format("long %d", l);
        case Double d  -> String.format("double %f", d);
        case String s  -> String.format("String %s", s);
        default        -> o.toString();
    };
}

The semantics of this switch are clear: A case label with a pattern matches the value of the selector expression o if the value matches the pattern. (We have shown a switch expression for brevity but could instead have shown a switch statement; the switch block, including the case labels, would be unchanged.)

The intent of this code is clearer because we are using the right control construct: We are saying, “the parameter o matches at most one of the following conditions, figure it out and evaluate the corresponding arm.” As a bonus, it is optimizable; in this case we are more likely to be able to perform the dispatch in O(1) time.

Pattern matching and null

Traditionally, switch statements and expressions throw NullPointerException if the selector expression evaluates to null, so testing for null must be done outside of the switch:

static void testFooBar(String s) {
    if (s == null) {
        System.out.println("oops!");
        return;
    }
    switch (s) {
        case "Foo", "Bar" -> System.out.println("Great");
        default           -> System.out.println("Ok");
    }
}

This was reasonable when switch supported only a few reference types. However, if switch allows a selector expression of any type, and case labels can have type patterns, then the standalone null test feels like boilerplate. It would be better to integrate the null test into the switch:

static void testFooBar(String s) {
    switch (s) {
        case null         -> System.out.println("Oops");
        case "Foo", "Bar" -> System.out.println("Great");
        default           -> System.out.println("Ok");
    }
}

The behavior of the switch when the value of the selector expression is null is always determined by its case labels. With a case null, the switch executes the code associated with that label; without a case null, the switch throws NullPointerException, just as before. (To maintain backward compatibility with the current semantics of switch, the default label does not match a null selector.)

We may wish to handle null in the same way as another case label. For example, in the following code, case null, String s would match both the null value and all String values:

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

Refining patterns in switch

Experimentation with patterns in switch suggests it is common to want to refine patterns. Consider the following code that switches over a Shape value:

class Shape {}
class Rectangle extends Shape {}
class Triangle  extends Shape { int calculateArea() { ... } }

static void testTriangle(Shape s) {
    switch (s) {
        case null:
            break;
        case Triangle t:
            if (t.calculateArea() > 100) {
                System.out.println("Large triangle");
                break;
            }
        default:
            System.out.println("A shape, possibly a small triangle");
    }
}

The intent of this code is to have a special case for large triangles (with area over 100), and a default case for everything else (including small triangles). However, we cannot express this directly with a single pattern. We first have to write a case label that matches all triangles, and then place the test of the area of the triangle rather uncomfortably within the corresponding statement group. Then we have to use fall-through to get the correct behavior when the triangle has an area less than 100. (Note the careful placement of break; inside the if block.)

The problem here is that using a single pattern to discriminate among cases does not scale beyond a single condition. We need some way to express a refinement to a pattern. One approach might be to allow case labels to be refined; such a refinement is called a guard in other programming languages. For example, we could introduce a new keyword where to appear at the end of a case label and be followed by a boolean expression, e.g., case Triangle t where t.calculateArea() > 100.

However, there is a more expressive approach. Rather than extend the functionality of case labels, we can extend the language of patterns themselves. We can add a new kind of pattern called a guarded pattern, written p && b, that allows a pattern p to be refined by an arbitrary boolean expression b.

With this approach, we can revisit the testTriangle code to express the special case for large triangles directly. This eliminates the use of fall-through in the switch statement, which in turn means we can enjoy concise arrow-style (->) rules:

static void testTriangle(Shape s) {
    switch (s) {
        case Triangle t && (t.calculateArea() > 100) ->
            System.out.println("Large triangle");
        default ->
            System.out.println("A shape, possibly a small triangle");
    }
}

The value of s matches the pattern Triangle t && (t.calculateArea() > 100) if, first, it matches the type pattern Triangle t and, if so, the expression t.calculateArea() > 100 evaluates to true.

Using switch makes it easy to understand and change case labels when application requirements change. For example, we might want to split triangles out of the default path; we can do that by using both a refined pattern and a non-refined pattern:

static void testTriangle(Shape s) {
    switch (s) {
        case Triangle t && (t.calculateArea() > 100) ->
            System.out.println("Large triangle");
        case Triangle t ->
            System.out.println("Small triangle");
        default ->
            System.out.println("Non-triangle");
    }
}

Description

We enhance switch statements and expressions in two ways:

  • Extend case labels to include patterns in addition to constants, and
  • Introduce two new kinds of patterns: guarded patterns and parenthesized patterns.

Patterns in case labels

We revise the grammar for switch labels in a switch block to read (compare JLS §14.11.1):

SwitchLabel:
  case CaseConstant { , CaseConstant }
  case Pattern
  case null
  case null, TypePattern
  case null, default
  default

A normal switch block is a switch block where one or more switch labels are case labels with constants, and all the other switch labels are case null, case null, default, or default. (That is, no case labels contain patterns.)

A normal switch is a switch statement or expression with a normal switch block.

A pattern switch block is a switch block where every switch label is either a case label with a pattern, or a case label with null and a type pattern, or case null, or case null, default. (That is, no case labels contain constants.)

A pattern switch is a switch statement or expression with a pattern switch block.

Less formally, a switch can have either patterns as labels (pattern switch) or constants as labels (normal switch), but not a mix. This partitioning is analogous to how a switch may contain arrow-style rules (->) or colon-style statement groups (:), but not a mix.

The grammar makes it clear that a case label may have multiple constants but only one pattern. This means that a case label in a normal switch is somewhat more expressive than a case label in a pattern switch, since only the former can share the same action across different values of the selector expression. This is an inevitable consequence of how patterns introduce names, which we consider further below.

Both a normal switch and a pattern switch allow either arrow–style rules or colond-style statement groups in the switch block.

The behavior of a pattern switch is, broadly, the same as the behavior of a normal switch: The value of the selector expression is compared to the switch labels, one of the labels is selected, and the code associated with that label is executed. The difference in a pattern switch is that selection is determined by pattern matching rather than by checking equality. For example, in the following code, the value of o will match the pattern Long l, and the code associated with case Long l will be executed:

Object o = 123L;
String formatted = switch (o) {
    case Integer i -> String.format("int %d", i);
    case Long l    -> String.format("long %d", l);
    case Double d  -> String.format("double %f", d);
    case String s  -> String.format("String %s", s);
    default        -> o.toString();
};

There are three major design issues when case labels can have patterns:

  1. Enhanced type checking
  2. Scope of pattern variable declarations
  3. Dealing with null

1. Enhanced type checking

1a. Selector expression typing

The type of the selector expression is broader in a pattern switch than in a normal switch. Namely:

  • The type of the selector expression of a normal switch must be either an integral primitive type (char, byte, short, or int), the corresponding boxed form (Character, Byte, Short, or Integer), String, or an enum type.
  • The type of the selector expression of a pattern switch can be either an integral primitive type or any reference type.

For example, in the following pattern switch the selector expression o is matched with type patterns involving a class type, an enum type, a record type, and an array type:

record Point(int i, int j) {}
enum Color { RED, GREEN, BLUE; }

static void typeTester(Object o) {
    switch (o) {
        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 ints of length" + ia.length);
        default       -> System.out.println("Something else");
    }
}

Every case label in the switch block must be compatible with the selector expression. For a case label with a pattern, known as a pattern label, we use the existing notion of compatibility of an expression with a pattern (JLS §14.30.1).

1b. Dominance of pattern labels

It is possible for the selector expression to match multiple patterns in a pattern switch block. Consider this problematic example:

static void error(Object o) {
    switch(o) {
        case CharSequence cs ->
            System.out.println("A sequence of length " + cs.length());
        case String s ->    // Error - pattern is dominated by previous pattern
            System.out.println("A string: " + s);

    }
}

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 vice versa. This is because the type of the second pattern, String, is a subtype of the type of the first pattern, CharSequence.

A pattern label of the form case p dominates a pattern label of the form case p && e, i.e., where the pattern is a guarded version of the original pattern. For example, the pattern label case String s dominates the pattern label case String s && s.length() > 0, since every value that matches the guarded pattern String s && s.length() > 0 also matches the pattern String s.

The compiler checks all pattern labels. It is a compile-time error if a pattern label in a switch block is dominated by an earlier pattern label in that switch block. (For this purpose, case null, T t is treated as if it were case T t.)

The notion of dominance is analogous to conditions on the catch clauses of a try statement, where it is an error if a catch clause that catches an exception class E is preceded by a catch clause that can catch E or a superclass of E (JLS §11.2.3). Logically, the preceding catch clause dominates the subsequent catch clause.

It is also a compile-time error if a switch block has more than one match-all switch label. The two match-all switch labels are default and case null, default.

1c. Completeness of pattern labels in switch expressions

A switch expression requires that all possible values of the selector expression are handled in the switch block. This maintains the property that successful evaluation of a switch expression will always yield a value. For normal switch expressions, this is enforced by a fairly straightforward set of extra conditions on the switch block. For pattern switch expressions, we define a notion of type coverage of a switch block.

Consider this (erroneous) pattern switch expression:

static int coverage(Object o) {
    return switch (o) {         // Error - incomplete
        case String s -> s.length();
    };
}

The switch block has only one case label, case String s. This matches any value of the selector expression whose type is a subtype of String. We therefore say that the type coverage of this arrow rule is every subtype of String. This pattern switch expression is incomplete because the type coverage of its switch block does not include the type of the selector expression.

Consider this (still erroneous) example:

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

The type coverage of this switch block is the union of the coverage of its two arrow rules. In other words, the type coverage is the set of all subtypes of String and the set of all subtypes of Integer. But, again, the type coverage still does not include the type of the selector expression, so this pattern switch expression is also incomplete and causes a compile-time error.

The type coverage of default is all types, so this example is (at last!) legal:

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

If the type of the selector expression is a sealed class (JEP 397), then the type coverage check can take into account the permits clause of the sealed class to determine whether a switch block is complete. Consider the following example of a sealed interface S with three permitted subclasses A, B, and C:

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 determine that the type coverage of the switch block is the types A, B, and C. Since the type of the selector expression, S, is a sealed interface whose permitted subclasses are exactly A, B, and C, this switch block is complete. As a result, no default label is needed.

To defend against incompatible separate compilation, the compiler automatically adds a default label whose code throws an IncompatibleClassChangeError. This label will only be reached if the sealed interface is changed and the switch code is not recompiled. In effect, the compiler hardens your code for you.

The requirement for a pattern switch expression to be complete is analogous to the treatment of a switch expression whose selector expression is an enum class, where a default label is not required if there is a clause for every constant of the enum class.

2. Scope of pattern variable declarations

Pattern variables (JEP 394) are local variables that are declared by patterns. Pattern variable declarations are unusual in that their scope is flow-sensitive. As a recap consider the following example, where the type pattern String s declares the pattern variable s:

static void test(Object o) {
    if ((o instanceof String s) && s.length() > 3) {
        System.out.println(s);
    } else {
        System.out.println("Not a string");
    }
}

The declaration of s is in scope in the right-hand operand of the && expression, as well as in the “then” block. However, it is not in scope in the “else” block; in order for control to transfer to the “else” block the pattern match must fail, in which case the pattern variable will not have been initialized.

We extend this flow-sensitive notion of scope for pattern variable declarations to encompass pattern declarations occurring in case labels with two new rules:

  1. The scope of a pattern variable declaration which occurs in a case label of a switch rule includes the expression, block, or throw statement that appears to the right of the arrow.
  2. The scope of a pattern variable declaration which occurs in a case label of a switch labeled statement group, where there are no further switch labels that follow, includes the block statements of the statement group.

This example shows the first rule in action:

static void test(Object o) {
    switch (o) {
        case Character c -> {
            if (c.charValue() == 7) {
                System.out.println("Ding!");
            }
            System.out.println("Character");
        }
        case Integer i ->
            throw new IllegalStateException("Invalid argument");
    }
}

The scope of the declaration of the pattern variable c is the block to the right of the first arrow.

The scope of the declaration of the pattern variable i is the throw statement to the right of the second arrow.

The second rule is more complicated. Let us first consider an example where there is only one case label for a switch labeled statement group:

static void test(Object o) {
    switch (o) {
        case Character c:
            if (c.charValue() == 7) {
                System.out.print("Ding ");
            }
            if (c.charValue() == 9) {
                System.out.print("Tab ");
            }
            System.out.println("character");
        default:
            System.out.println();
    }
}

The scope of the declaration of the pattern variable c includes all the statements of the statement group, namely the two if statements and the println statement. The scope does not include the statements of the default statement group, even though the execution of the first statement group can fall through the default switch label and execute these statements.

The possibility of falling through a case label that declares a pattern variable must be excluded as a compile-time error. Consider this erroneous example:

static void test(Object o) {
    switch (o) {
        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);
    }
}

If this were allowed and the value of the selector expression o was a Character, then execution of the switch block could fall through the second statement group (after case Integer i:) where the pattern variable i would not have been initialized. Allowing execution to fall through a case label that declares a pattern variable is therefore a compile-time error.

This is why case Character c: case Integer i: ... is not permitted. Similar reasoning applies to the prohibition of multiple patterns in a case label: Neither case Character c, Integer i: ... nor case Character c, Integer i -> ... is allowed. If such case labels were allowed then both c and i would be in scope after the colon or arrow, yet only one of c and i would have been initialized depending on whether the value of o was a Character or an Integer.

On the other hand, falling through a label that does not declare a pattern variable is safe, as this example shows:

void test(Object o) {
    switch (o) {
        case String s:
            System.out.println("A string");
        default:
            System.out.println("Done");
    }
}

3. Dealing with null

Traditionally, a switch throws NullPointerException if the selector expression evaluates to null. This is well-understood behavior and we do not propose to change it for any existing switch code.

However, given that there is a reasonable and non-exception-bearing semantics for pattern matching and null values, there is an opportunity to make pattern switch more null-friendly while remaining compatible with existing switch semantics.

We introduce three new null-matching case labels, only one of which may occur in any given switch block:

  1. case null — matches when the value of the selector expression is null.
  2. case null, T t — matches when the value of the selector is null, or it matches the type pattern T t.
  3. case null, default — matches when the value of the selector is null, or if no other case labels match.

We lift the blanket rule that a switch immediately throws NullPointerException if the value of the selector expression is null. Instead, we inspect the case labels to determine the behavior of a switch:

  • If the selector expression evaluates to null then any of the three null-matching labels are said to match. If there is no null-matching label associated with the switch block then the switch throws NullPointerException, as before.
  • If the selector expression evaluates to a non-null value then we select a matching case label, as normal. If no case label matches then any match-all label is considered to match.

For example, given the declaration below, evaluating test(null) will print null! rather than throw NullPointerException:

static void test(Object o) {
    switch (o) {
        case null     -> System.out.println("null!");
        case String s -> System.out.println("String");
        default       -> System.out.println("Something else");
    }
}

This new behavior around null is as if the compiler automatically enriches the switch block with a case null whose body throws NullPointerException. In other words, this code:

static void test(Object o) {
    switch (o) {
        case String s  -> System.out.println("String: " + s);
        case Integer i -> System.out.println("Integer");
    }
}

is equivalent to:

static void test(Object o) {
    switch (o) {
        case null      -> throw new NullPointerException();
        case String s  -> System.out.println("String: "+s);
        case Integer i -> System.out.println("Integer");
    }
}

In both examples, evaluating test(null) will cause NullPointerException to be thrown.

We preserve the intuition from the existing switch construct that performing a switch over null is an exceptional thing to do. The difference in a pattern switch is that you have a mechanism to directly handle this case inside the switch rather than outside. If you choose not to have a null-matching case label in a switch block then switching over null value will throw NullPointerException, as before.

Guarded and parenthesized patterns

After a successful pattern match we often further test the result of the match. This can lead to cumbersome code, such as:

static void test(Object o) {
    switch (o) {
        case String s:
            if (s.length() == 1) { ... }
            else { ... }
            break;
        ...
    }
}

The desired test — that o is a String of length 1 — is unfortunately split between the case label and the ensuing if statement. We could improve readability if a pattern switch supported the combination of a pattern and a boolean expression in a case label.

Rather than add another special case label, we enhance the pattern language by adding guarded patterns, written p && e. This allows the above code to be rewritten so that all the conditional logic is lifted into the case label:

static void test(Object o) {
    switch (o) {
        case String s && (s.length() == 1) -> ...
        case String s                      -> ...
    }
}

The first case matches if o is both a String and of length 1. The second case matches if o is a String of some other length.

Sometimes we need to parenthesize patterns to avoid parsing ambiguities. We therefore extend the language of patterns to support parenthesized patterns written (p), where p is a pattern.

More precisely, we change the grammar of patterns. Assuming that the record patterns and array patterns of JEP 405 are added, the grammar for patterns will become:

Pattern:
  PrimaryPattern
  GuardedPattern

GuardedPattern:
  PrimaryPattern && ConditionalAndExpression

PrimaryPattern:
  TypePattern
  RecordPattern
  ArrayPattern
  ( Pattern )

A guarded pattern is of the form p && e, where p is a pattern and e is a boolean expression. In a guarded pattern any local variable, formal parameter, or exceptional parameter that is used but not declared in the subexpression must either be final or effectively final.

A guarded pattern p && e introduces the union of the pattern variables introduced by pattern p and expression e. The scope of any pattern variable declaration in p includes the expression e. This allows for patterns such as String s && (s.length() > 1), which matches a value that can be cast to a String such that the string has a length greater than one.

A value matches a guarded pattern p && e if, first, it matches the pattern p and, second, the expression e evaluates to true. If the value does not match p then no attempt is made to evaluate the expression e.

A parenthesized pattern is of the form (p), where p is a pattern. A parenthesized pattern (p) introduces the pattern variables that are introduced by the subpattern p. A value matches a parenthesized pattern (p) if it matches the pattern p.

We also change the grammar for instanceof expressions to:

InstanceofExpression:
  RelationalExpression instanceof ReferenceType
  RelationalExpression instanceof PrimaryPattern

This change, and the non-terminal ConditionalAndExpression in the grammar rule for a guarded pattern, ensure that, for example, the expression e instanceof String s && s.length() > 1 continues to unambiguously parse as the expression (e instanceof String s) && (s.length() > 1). If the trailing && is intended to be part of a guarded pattern then the entire pattern should be parenthesized, e.g., e instanceof (String s && s.length() > 1).

The use of the non-terminal ConditionalAndExpression in the grammar rule for a guarded pattern also removes another potential ambiguity concerning a case label with a guarded pattern. For example:

boolean b = true;
switch (o) {
    case String s && b -> s -> s;
}

If the guard expression of a guarded pattern were allowed to be an arbitrary expression then there would be an ambiguity as to whether the first occurrence of -> is part of a lambda expression or part of the switch rule, whose body is a lambda expression. Since a lambda expression can never be a valid boolean expression, it is safe to restrict the grammar of the guard expression.

Future work

  • At the moment, a pattern switch does not support the primitive types boolean, float, and double. Their utility seems minimal, but support for these could be added.

  • We expect that, in the future, general classes will be able to declare deconstruction patterns to specify how they can be matched against. Such deconstruction patterns can be used with a pattern switch to yield very succinct code. For example, if we have a hierarchy of Expr with subtypes for IntExpr (containing a single int), AddExpr and MulExpr (containing two Exprs), and NegExpr (containing a single Expr), we can match against an Expr and act on the specific subtypes all in one step:

    int eval(Expr n) {
         return switch(n) {
             case IntExpr(int i) -> i;
             case NegExpr(Expr n) -> -eval(n);
             case AddExpr(Expr left, Expr right) -> eval(left) + eval(right);
             case MulExpr(Expr left, Expr right) -> eval(left) * eval(right);
             default -> throw new IllegalStateException();
         };
    }
    

    Without such pattern matching, expressing ad-hoc polymorphic calculations like this requires using the cumbersome [visitor pattern][visitor]. Pattern matching is generally more transparent and straightforward.

  • It may also be useful to add AND and OR patterns, to allow more expressivity for case labels with patterns.

Alternatives

  • Rather than support a pattern switch we could instead define a type switch that just supports switching on the type of the selector expression. This feature is simpler to specify and implement but considerably less expressive.

  • There are many other syntactic options for guarded patterns, such as p where e, p when e, p if e, or even p &&& e.

  • An alternative to guarded patterns is to support guards directly as a special form of case label:

    SwitchLabel:
      case Pattern [ when Expression ]
      ...
    

    Supporting guards in case labels requires introducing when as a new contextual keyword, whereas guarded patterns do not require new contextual keywords or operators. Guarded patterns offer considerably more flexibility, since a guarded pattern can occur near where it applies rather than at the end of the switch label.

Dependencies

This JEP builds on pattern matching for instanceof (JEP 394) and also the enhancements offered by switch expressions (JEP 361). We intend this JEP to coincide with JEP 405, which defines two new kinds of patterns that support nesting. The implementation will likely make use of dynamic constants (JEP 309).