This specification is not final and is subject to change. Use is subject to license terms.

Pattern Matching for switch (Second Preview)

Changes to the Java® Language Specification • Version 18-internal+0-adhoc.gbierman.20211020

This document describes changes to the Java Language Specification to support Pattern Matching for switch, a preview feature of Java SE 18. See JEP 420 for an overview of the feature.

Changes are described with respect to existing sections of the JLS. New text is indicated like this and deleted text is indicated like this. Explanation and discussion, as needed, is set aside in grey boxes.

Changelog:

2021-10-20: First draft released. Main changes from JEP 405 preview feature, in addition to various bug-fixes, are:

Chapter 6: Names

6.3 Scope of a Declaration

6.3.1 Scope for Pattern Variables in Expressions

6.3.1.6 switch Expressions

The following rule applies rules apply to a switch expression (15.28) with a switch block consisting of switch rules (14.11.1):

The following rules apply to a switch expression with a switch block consisting of switch labeled statement groups (14.11.1):

6.3.2 Scope for Pattern Variables in Statements

6.3.2.6 switch Statements

The following rule applies rules apply to a switch statement (14.11) with a switch block consisting of switch rules (14.11.1):

The following rules apply to a switch expression with a switch block consisting of switch labeled statement groups (14.11.1):

6.3.3 Scope for Pattern Variables in Patterns

6.3.3.1 Guarded Pattern

The following rule applies to a guarded pattern p && e:

6.3.4 Scope for Pattern Variables in Switch Labels

Pattern variables can be introduced by switch labels that have patterns, and are in scope for the relevant parts of the associated switch expression (6.3.1.6) or switch statement (6.3.2.6).

The following rule applies to a switch label:

Chapter 14: Blocks, Statements, and Patterns

14.11 The switch Statement

The switch statement transfers control to one of several statements or expressions, depending on the value of an expression.

SwitchStatement:
switch ( Expression ) SwitchBlock

The Expression is called the selector expression. The type of the selector expression must be char, byte, short, int, Character, Byte, Short, Integer, String, or an enum type (8.9), or a compile-time error occurs.

These restrictions on the type of the selector expression are now included in the notion of a switch block being compatible with a selector expression, defined in the following section.

14.11.1 Switch Blocks

The body of both a switch statement and a switch expression (15.28) is called a switch block. This subsection presents general rules which apply to all switch blocks, whether they appear in switch statements or switch expressions. Other subsections present additional rules which apply either to switch blocks in switch statements (14.11.2) or to switch blocks in switch expressions (15.28.1).

SwitchBlock:
{ SwitchRule {SwitchRule} }
{ {SwitchBlockStatementGroup} {SwitchLabel :} }
SwitchRule:
SwitchLabel -> Expression ;
SwitchLabel -> Block
SwitchLabel -> ThrowStatement
SwitchBlockStatementGroup:
SwitchLabel : {SwitchLabel :} BlockStatements
SwitchLabel:
case CaseConstant {, CaseConstant}
default
SwitchLabel:
CaseOrDefaultLabel {: CaseOrDefaultLabel }
CaseOrDefaultLabel:
case CaseLabelElement {, CaseLabelElement }
default
CaseLabelElement:
CaseConstant
Pattern
null
default
CaseConstant:
ConditionalExpression

A switch block can consist of either:

Every switch rule and switch labeled statement group starts with a switch label, which is either a case label or a default label. uses one or more case or default labels. A case label has one or more case label elements. Multiple switch labels are permitted for a switch labeled statement group. A switch label has a case label element, if it uses a case label that has that case label element.

A case label has one or more case constants. Every case constant must be either a constant expression (15.29) or the name of an enum constant (8.9.1), or a compile-time error occurs.

Switch labels and their case constants are said to be associated with the switch block. No two of the case constants associated with a switch block may have the same value, or a compile-time error occurs.

If a switch label appears at the end of a switch block, it is a compile-time error if it consists of more than one case or default label.

It is a compile-time error if the switch label of a switch rule consists of more than one case or default label.

This means that case 1: case 2 -> ... is not a valid switch rule, but can be written as case 1, 2 -> ....

For every switch label in a switch block, all of the following must be true, otherwise a compile-time error occurs:

These rules restrict the form of switch labels. Much of the complication is due to supporting the two ways of combining case label elements in switch labels for statement groups (for example case 1: case 2 and case 1,2).

A switch label is called a default switch label if it either uses a default label or has a default case label element.

A switch label is said to dominate another switch label if there are values for which both apply and there is not an obvious preference. The rules for determining dominance are as follows:

It is a compile-time error if a switch label in a switch block dominates any switch label that follows it in the switch block.

It is a compile-time error if there is a statement in a switch block that consists of switch-labeled statement groups for which both of the following are true:

  1. It is labeled with a switch label that has a pattern case label element whose pattern introduces a pattern variable.

  2. There is a statement preceding it in the switch block and that statement can completely normally (14.22).

This condition is required to exclude the possibility of a switch labeled statement being reached for which a pattern variable declared in its switch label is in scope but without the pattern matching having succeeded. For example, the statement labeled by the switch label that has the type pattern Integer i could be reached from the preceding statement group, and so the pattern variable i will not be initialized:

Object o = "Hello";
switch (o) {
    case String s:  
        System.out.println("String: " + s ); 
    case Integer i: 
        System.out.println(i + 1);    // Error! Can be reached 
                                      // without matching switch label
}

The switch block of a switch statement or a switch expression is compatible with the type of the selector expression, T e, if both of the following are true all the case labels used by the switch labels in the switch block are compatible with e. A case label is compatible with e if every case label element it has is compatible with e, as follows:

The switch block of a switch statement or a switch expression must be compatible with the type of the selector expression, or a compile-time error occurs.

It is a compile-time error if both of the following are true for a switch expression or a switch statement:

  1. There is a default switch label in the switch block, and

  2. There is a switch label in the switch block that has a pattern case label element whose pattern is total for the type of the selector expression (14.30.3).

A pattern that is total for the type of the selector expression will match every value, and so behaves much like a default switch label.

A type T supports a sealed class or interface C if and only if one of the following holds:

A switch block is exhaustive for a type T if one of the following is true:

A switch statement or expression is exhaustive if its switch block is exhaustive for the type of the selector expression.

As the meaning of some patterns is determined by the type of the expression that are being matching against, patterns appearing in switch labels must be resolved (14.30.2).

Resolving a switch block at type T results in an identical switch block except where every switch label L is replaced with the result of resolving L at type T.

Resolving a switch label L at type T proceeds by resolving all the default and case labels it uses as follows:

Both the execution of a switch statement (14.11.3) and the evaluation of a switch expression (15.28.2) need to determine if a switch label in a resolved switch block matches applies to the value of the selector expression. To determine Determining whether a switch label in a resolved switch block matches applies to a given value, the value is compared with the case constants associated with the switch block is as follows: Then:

A case switch label can contain have several case constants constant case label elements. The label matches applies to the value of the selector expression if any one of its constants matches is equal to the value of the selector expression. For example, in the following code, the case switch label matches if the enum variable day is either one of the enum constants shown:

switch (day) {
    ...
    case SATURDAY, SUNDAY :
        System.out.println("It's the weekend!");
        break;
    ...
}

If a switch label that has a pattern case label element applies, then this is because the process of pattern matching the value against the pattern has succeeded (14.30.2). If a value successfully matches a pattern then the process of pattern matching initializes any pattern variables declared by the pattern.

null cannot be used as a case constant because it is not a constant expression. Even if case null was allowed, it would be undesirable because the code in that case can never be executed. Namely, if the selector expression is of a reference type (that is, String or a boxed primitive type or an enum type), then an exception will occur if the selector expression evaluates to null at run time. In the judgment of the designers of the Java programming language, propagating the exception is a better outcome than either having no case label match, or having the default label match.

In C and C++ the body of a switch statement can be a statement and statements with case labels do not have to be immediately contained by that statement. Consider the simple loop:

for (i = 0; i < n; ++i) foo();

where n is known to be positive. A trick known as Duff's device can be used in C or C++ to unroll the loop, but this is not valid code in the Java programming language:

int q = (n+7)/8;
switch (n%8) {
    case 0: do { foo();    // Great C hack, Tom,
    case 7:      foo();    // but it's not valid here.
    case 6:      foo();
    case 5:      foo();
    case 4:      foo();
    case 3:      foo();
    case 2:      foo();
    case 1:      foo();
            } while (--q > 0);
}

Fortunately, this trick does not seem to be widely known or used. Moreover, it is less needed nowadays; this sort of code transformation is properly in the province of state-of-the-art optimizing compilers.

14.11.2 The Switch Block of a switch Statement

In addition to the general rules for switch blocks (14.11.1), there are further rules for switch blocks in switch statements.

An enhanced switch statement is one where either (i) the type of the selector expression is not char, byte, short, int, Character, Byte, Short, Integer, String, or an enum type, or (ii) at least one of the switch labels has a pattern case label element or a null case label element.

Namely, all All of the following must be true for the switch block of a switch statement, or a compile-time error occurs:

Prior to Java SE 17, switch statements (and switch expressions) were limited in two ways: (i) the type of the selector expression was restricted to an integral type, an enum type, or String; and (ii) only constant case label elements were supported. Moreover, unlike switch expressions, switch statements did not have to be exhaustive. This is often the cause of difficult to detect bugs, where no switch label applies and the switch statement will silently do nothing. For example:

enum E { A, B, C}

E e = ...;
switch (e) {
   case A -> System.out.println("A");
   case B -> System.out.println("B");
   // No case for C!
}

With Java SE 17, switch statements have been enhanced in the sense that the two limitations listed above have been lifted. The designers of the Java programming language decided that enhanced switch statements should align with switch expressions and be required to be exhaustive. This is often achieved with the addition of a trivial default switch label. For example, the following enhanced switch statement is not exhaustive:

Object o = ...;
switch (o) {    // Error - non-exhaustive switch!
    case String s -> System.out.println("A string!");
}

but it can easily be made exhaustive:

Object o = ...;
switch (o) {    
    case String s -> System.out.println("A string!");
    default -> {}
}

For compatibility reasons, switch statements that are not an enhanced switch statement are not required to be exhaustive.

One consequence of requiring exhaustiveness is that an enhanced switch statement, unlike a normal switch statement, can not have an empty switch block.

14.11.3 Execution of a switch Statement

Execution of a switch statement is always with respect to a resolved switch block (14.11.1). The switch block is resolved at type T, where T is the type of the selector expression.

A switch statement is executed by first evaluating the selector expression. Then:

If evaluation of the selector expression completes normally and the result is non-null, and the subsequent unboxing conversion (if any) completes normally, then execution of the switch statement continues by determining if a switch label associated with in the resolved switch block matches applies to the value of the selector expression (14.11.1). Then:

If execution of any statement or expression in the switch block completes abruptly, it is handled as follows:

Example 14.11.3-1. Fall-Through in the switch Statement

When a selector expression matches a switch label switch label applies, and that switch label is for a switch rule, the switch rule expression or statement introduced by the switch label is executed, and nothing else. In the case of a switch label for a statement group, all the block statements in the switch block that follow the switch label are executed, including those that appear after subsequent switch labels. The effect is that, as in C and C++, execution of statements can "fall through labels."

For example, the program:

class TooMany {
    static void howMany(int k) {
        switch (k) {
            case 1: System.out.print("one ");
            case 2: System.out.print("too ");
            case 3: System.out.println("many");
        }
    }
    public static void main(String[] args) {
        howMany(3);
        howMany(2);
        howMany(1);
    }
}

contains a switch block in which the code for each case falls through into the code for the next case. As a result, the program prints:

many
too many
one too many

Fall through can be the cause of subtle bugs. If code is not to fall through case to case in this manner, then break statements can be used to indicate when control should be transferred, or switch rules can be used, as in the program:

class TwoMany {
    static void howMany(int k) {
        switch (k) {
            case 1: System.out.println("one");
                    break;  // exit the switch
            case 2: System.out.println("two");
                    break;  // exit the switch
            case 3: System.out.println("many");
                    break;  // not needed, but good style
        }
    }
    static void howManyAgain(int k) {
        switch (k) {
            case 1 -> System.out.println("one");
            case 2 -> System.out.println("two");
            case 3 -> System.out.println("many");
        }
    }
    public static void main(String[] args) {
        howMany(1);
        howMany(2);
        howMany(3);
        howManyAgain(1);
        howManyAgain(2);
        howManyAgain(3);    
    }
}

This program prints:

one
two
many
one
two
many

14.30 Patterns

A pattern describes a test that can be performed on a value. Patterns appear as operands of statements and expressions, which provide the values to be tested. Patterns declare local variables, known as pattern variables.

The process of testing a value against a pattern is known as pattern matching. If a value successfully matches a pattern, then the process of pattern matching initializes the pattern variable declared by the pattern.

Pattern variables are only in scope (6.3) where pattern matching succeeds and thus the pattern variables will have been initialized. It is not possible to use a pattern variable that has not been initialized.

14.30.1 Kinds of Patterns

Primary patterns are the simplest forms of patterns, from which all others are constructed. A type pattern is used to test whether a value is an instance of the type appearing in the pattern. A parenthesized pattern is also treated syntactically as a primary pattern.

A guarded pattern consists of a contained primary pattern and a contained guarding expression and is used to test whether a value matches the pattern and additionally that the guarding boolean expression is true.

Pattern:
TypePattern
Pattern:
PrimaryPattern
GuardedPattern
GuardedPattern:
PrimaryPattern && ConditionalAndExpression
PrimaryPattern:
TypePattern
( Pattern )
TypePattern:
LocalVariableDeclaration

The following productions from 4.3, 8.3, 8.4.1, and 14.4 are shown here for convenience:

LocalVariableDeclaration:
{VariableModifier} LocalVariableType VariableDeclaratorList
VariableModifier:
Annotation
final
LocalVariableType:
UnannType
var
VariableDeclaratorList:
VariableDeclarator {, VariableDeclarator}
VariableDeclarator:
VariableDeclaratorId [= VariableInitializer]
VariableDeclaratorId:
Identifier [Dims]
Dims:
{Annotation} [ ] {{Annotation} [ ]}

See 8.3 for UnannType.

There is also a special any pattern, which is a pattern that arises from the process of resolving a pattern (14.30.2).

Currently, any patterns may not appear in a pattern instanceof expression, or in a pattern label of a switch expression or switch statement. It is possible that future versions of the Java programming language may relax this restriction.

A type pattern declares one local variable, known as a pattern variable. The Identifier in the local variable declaration specifies the name of the pattern variable.

The rules for a local variable declared in a type pattern are specified in 14.4. In addition, all of the following must be true, or a compile-time error occurs:

The type of a pattern variable is the reference type denoted by LocalVariableType.

The type of a type pattern is the type of its pattern variable.

A parenthesized pattern declares the local variables that are declared by the contained pattern.

The type of a parenthesized pattern is the type of the contained pattern.

An any pattern declares one local variable, known as a pattern variable.

The pattern variable of an any pattern has a type, which is a reference type.

The type of an any pattern is the type of its pattern variable.

A pattern variable is declared by a guarded pattern p&&e if one of the following is true:

It is a compile-time error if any pattern variable is both declared by the contained pattern and by the guarding expression of a guarded pattern when true.

Any variable that is used but not declared in the guarding expression of a guarded pattern must either be final or effectively final (4.12.4).

The type of a guarded pattern is the type of the contained pattern.

An expression e is compatible with a pattern of type T if e is downcast compatible with T (5.5).

An expression e is compatible with a pattern if one of the following is true:

Compatibility of an expression with a pattern is used by the instanceof pattern match operator (15.20.2) and a switch expression or switch statement using patterns in a switch label (14.11.1).

14.30.2 Pattern Matching

Pattern matching is the process of testing a value against a pattern at run time. Pattern matching is distinct from statement execution (14.1) and expression evaluation (15.1). If a value successfully matches a pattern, then the process of pattern matching initializes the pattern variable declared by the pattern.

Before pattern matching is performed, patterns are first resolved with respect to the type of the expression that they are to be matched against, resulting in a possibly amended pattern.

The rules for resolving a pattern at type U are as follows:

This process of resolving a pattern addresses a problem that arises when matching a null reference value against a type pattern. Checking whether a value matches a type pattern of type U involves checking (at runtime) whether the value can be cast to U. But the null reference can be cast to any reference type without any checks. If an expression of type V ultimately evaluates to the null reference, we'd still expect the match to only succeed if V was a subtype of the type of the type pattern.

This suggests that pattern matching needs to involve compile-time types. Rather than directly defining the runtime process of pattern matching with reference to compile-time types, we instead observe that pattern matching should satisfy the following property: the value of any expression whose static type is T always matches a type pattern of type S, where T is a subtype of S.

Thus, at compile-time we "resolve" the pattern with respect to the (compile-time) type of the expression being pattern matched. If it can be determined at compile-time that a pattern will always match, it is translated, or resolved, to a special any pattern (which, by definition, all values match without any examination of runtime types). This ensures the expected behavior when the value being matched is the null reference. In the other cases, resolving a pattern leaves the pattern untouched.

Some patterns contain expressions which are evaluated during pattern matching. Pattern matching is said to complete abruptly if evaluation of a contained expression completes abruptly. An abrupt completion always has an associated reason, which is always a throw with a given value. Pattern matching is said to complete normally if it does not complete abruptly.

The rules for determining whether a value matches a pattern, and for initializing pattern variables, are as follows:

There is no rule to cover a value that is the null reference. This is because the solitary construct that performs pattern matching, the instanceof pattern match operator (15.20.2), only does so when a value is not the null reference. It is possible that future versions of the Java programming language will allow pattern matching in other expressions and statements.

14.30.3 Pattern Totality and Dominance

A pattern is said to be total for a type T as follows:

A pattern p is said to dominate a pattern q if every value that matches q also matches p, and is defined as follows:

Chapter 15: Expressions

15.20 Relational Operators

15.20.2 The instanceof Operator

An instanceof expression may perform either type comparison or pattern matching.

InstanceofExpression:
RelationalExpression instanceof ReferenceType
RelationalExpression instanceof Pattern PrimaryPattern

If the operand to the right of the instanceof keyword is a ReferenceType, then the instanceof keyword is the type comparison operator.

If the operand to the right of the instanceof keyword is a Pattern PrimaryPattern, then the instanceof keyword is the pattern match operator.

The following rules apply when instanceof is the type comparison operator:

The following rules apply when instanceof is the pattern match operator:

Example 15.20.2-1. The Type Comparison Operator

class Point   { int x, y; }
class Element { int atomicNumber; }
class Test {
    public static void main(String[] args) {
        Point   p = new Point();
        Element e = new Element();
        if (e instanceof Point) {  // compile-time error
            System.out.println("I get your point!");
            p = (Point)e;  // compile-time error
        }
    }
}

This program results in two compile-time errors. The cast (Point)e is incorrect because no instance of Element or any of its possible subclasses (none are shown here) could possibly be an instance of any subclass of Point. The instanceof expression is incorrect for exactly the same reason. If, on the other hand, the class Point were a subclass of Element (an admittedly strange notion in this example):

class Point extends Element { int x, y; }

then the cast would be possible, though it would require a run-time check, and the instanceof expression would then be sensible and valid. The cast (Point)e would never raise an exception because it would not be executed if the value of e could not correctly be cast to type Point.

Prior to Java SE 16, the ReferenceType operand of a type comparison operator was required to be reifiable (4.7). This prevented the use of a parameterized type unless all its type arguments were wildcards. The requirement was lifted in Java SE 16 to allow more parameterized types to be used. For example, in the following program, it is legal to test whether the method parameter x, with static type List<Integer>, has a more "refined" parameterized type ArrayList<Integer> at run time:

import java.util.ArrayList;
import java.util.List;

class Test2 {
    public static void main(String[] args) {
        List<Integer> x = new ArrayList<Integer>();
    
        if (x instanceof ArrayList<Integer>) {  // OK
            System.out.println("ArrayList of Integers");
        }
        if (x instanceof ArrayList<String>) {  // error
            System.out.println("ArrayList of Strings");
        }
        if (x instanceof ArrayList<Object>) {  // error
            System.out.println("ArrayList of Objects");
        }
    }
}

The first instanceof expression is legal because there is a casting conversion from List<Integer> to ArrayList<Integer>. However, the second and third instanceof expressions both cause a compile-time error because there is no casting conversion from List<Integer> to ArrayList<String> or ArrayList<Object>.

15.28 switch Expressions

A switch expression transfers control to one of several statements or expressions, depending on the value of an expression; all possible values of that expression must be handled, and all of the several statements and expressions must produce a value for the result of the switch expression.

SwitchExpression:
switch ( Expression ) SwitchBlock

The Expression is called the selector expression. The type of the selector expression must be char, byte, short, int, Character, Byte, Short, Integer, String, or an enum type (8.9), or a compile-time error occurs.

The body of both a switch expression and a switch statement (14.11) is called a switch block. General rules which apply to all switch blocks, whether they appear in switch expressions or switch statements, are given in 14.11.1. The following productions from 14.11.1 are shown here for convenience:

SwitchBlock:
{ SwitchRule {SwitchRule} }
{ {SwitchBlockStatementGroup} {SwitchLabel :} }
SwitchRule:
SwitchLabel -> Expression ;
SwitchLabel -> Block
SwitchLabel -> ThrowStatement
SwitchBlockStatementGroup:
SwitchLabel : {SwitchLabel :} BlockStatements
SwitchLabel:
case CaseConstant {, CaseConstant}
default
SwitchLabel:
CaseOrDefaultLabel {: CaseOrDefaultLabel }
CaseOrDefaultLabel:
case CaseLabelElement {, CaseLabelElement }
default
CaseLabelElement:
CaseConstant
Pattern
null
default
CaseConstant:
ConditionalExpression

15.28.1 The Switch Block of a switch Expression

In addition to the general rules for switch blocks (14.11.1), there are further rules for switch blocks in switch expressions. Namely, all of the following must be true for the switch block of a switch expression, or a compile-time error occurs:

switch expressions cannot have empty switch blocks, unlike switch statements. Furthermore, switch expressions differ from switch statements in terms of which expressions may appear to the right of an arrow (->) in the switch block, that is, which expressions may be used as switch rule expressions. In a switch expression, any expression may be used as a switch rule expression, but in a switch statement, only a statement expression may be used (14.11.1).

A switch expression must be exhaustive (14.11.1), or a compile-time error occurs.

The result expressions of a switch expression are determined as follows:

It is a compile-time error if a switch expression has no result expressions.

A switch expression is a poly expression if it appears in an assignment context or an invocation context (5.2, 5.3). Otherwise, it is a standalone expression.

Where a poly switch expression appears in a context of a particular kind with target type T, its result expressions similarly appear in a context of the same kind with target type T.

A poly switch expression is compatible with a target type T if each of its result expressions is compatible with T.

The type of a poly switch expression is the same as its target type.

The type of a standalone switch expression is determined as follows:

15.28.2 Run-Time Evaluation of switch Expressions

Evaluation of a switch expression is always with respect to a resolved switch block (14.11.1). The switch block is resolved at type T, where T is the type of the selector expression.

A switch expression is evaluated by first evaluating the selector expression. Then:

If evaluation of the selector expression completes normally and the result is non-null, and the subsequent unboxing conversion (if any) completes normally, then evaluation of the switch expression continues by determining if a switch label associated with in the resolved switch block matches applies to the value of the selector expression (14.11.1). Then:

If execution of any statement or expression in the switch block completes abruptly, it is handled as follows:

Chapter 16: Definite Assignment

16.2 Definite Assignment and Statements

16.2.9 switch Statements