Pattern Matching for switch (Third Preview) and Record Patterns (Preview)

Changes to the Java® Language Specification • Version 19+36-2238

This document describes changes to the Java Language Specification to support Pattern Matching for switch, and Record Patterns, which are both preview features of Java SE 19. See JEP 427 and JEP 405 respectively for overviews of the features.

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:

2022-06-01: - 14.11.1.2 Fixed treatment of how null case labels are resolved and determined to apply at run time. - 14.30.2 Fixed semantics of pattern matching for any patterns.

2022-05-17: 15.20.2 Added missing text about resolving the pattern (which was explicitly discussed in 14.30.2)

2022-05-10: 14.30.2 Require an instance of MatchException resulting from a record accessor completing abruptly explicitly record this reason as a cause.

2022-04-26: Updated to JEP 427 (Pattern Matching for switch (Third Preview)).

2022-04-25: Fixed some code examples and renamed "unrefined" pattern labels as "unguarded". Clarified text surrounding default switch labels.

2022-04-20: Second draft with corrections following feedback on amber-spec-experts mailing list.

2022-04-07: First draft released. Main changes from JEP 420 preview, in addition to various bug-fixes, are:

Chapter 3: Lexical Structure

3.9 Keywords

51 character sequences, formed from ASCII characters, are reserved for use as keywords and cannot be used as identifiers (3.8). Another 16 character sequences, also formed from ASCII characters, may be interpreted as keywords or as other tokens, depending on the context in which they appear.

Keyword:
ReservedKeyword
ContextualKeyword
ReservedKeyword:
(one of)
abstract continue for new switch
assert default if package synchronized
boolean do goto private this
break double implements protected throw
byte else import public throws
case enum instanceof return transient
catch extends int short try
char final interface static void
class finally long strictfp volatile
const float native super while
_ (underscore)
ContextualKeyword:
(one of)
exports permits to with
module provides transitive yield
non-sealed record uses
open requires var
opens sealed when

The keywords const and goto are reserved, even though they are not currently used. This may allow a Java compiler to produce better error messages if these C++ keywords incorrectly appear in programs.

The keyword strictfp is obsolete and should not be used in new code.

The keyword _ (underscore) is reserved for possible future use in parameter declarations.

true and false are not keywords, but rather boolean literals (3.10.3).

null is not a keyword, but rather the null literal (3.10.8).

During the reduction of input characters to input elements (3.5), a sequence of input characters that notionally matches a contextual keyword is reduced to a contextual keyword if and only if both of the following conditions hold:

  1. The sequence is recognized as a terminal specified in a suitable context of the syntactic grammar (2.3), as follows:

    • For module and open, when recognized as a terminal in a ModuleDeclaration (7.7).

    • For exports, opens, provides, requires, to, uses, and with, when recognized as a terminal in a ModuleDirective.

    • For transitive, when recognized as a terminal in a RequiresModifier.

      For example, recognizing the sequence requires transitive ; does not make use of RequiresModifier, so the term transitive is reduced here to an identifier and not a contextual keyword.

    • For var, when recognized as a terminal in a LocalVariableType (14.4) or a LambdaParameterType (15.27.1).

      In other contexts, attempting to use var as an identifier will cause an error, because var is not a TypeIdentifier (3.8).

    • For yield, when recognized as a terminal in a YieldStatement (14.21).

      In other contexts, attempting to use the yield as an identifier will cause an error, because yield is neither a TypeIdentifier nor a UnqualifiedMethodIdentifier.

    • For record, when recognized as a terminal in a RecordDeclaration (8.10).

    • For non-sealed, permits, and sealed, when recognized as a terminal in a NormalClassDeclaration (8.1) or a NormalInterfaceDeclaration (9.1).

    • For when, when recognized as a terminal in a SwitchBlock (14.11.1).

  2. The sequence is not immediately preceded or immediately followed by an input character that matches JavaLetterOrDigit.

In general, accidentally omitting white space in source code will cause a sequence of input characters to be tokenized as an identifier, due to the "longest possible translation" rule (3.2). For example, the sequence of twelve input characters p u b l i c s t a t i c is always tokenized as the identifier publicstatic, rather than as the reserved keywords public and static. If two tokens are intended, they must be separated by white space or a comment.

The rule above works in tandem with the "longest possible translation" rule to produce an intuitive result in contexts where contextual keywords may appear. For example, the sequence of eleven input characters v a r f i l e n a m e is usually tokenized as the identifier varfilename, but in a local variable declaration, the first three input characters are tentatively recognized as the contextual keyword var by the first condition of the rule above. However, it would be confusing to overlook the lack of white space in the sequence by recognizing the next eight input characters as the identifier filename. (This would mean that the sequence undergoes different tokenization in different contexts: an identifier in most contexts, but a contextual keyword and an identifier in local variable declarations.) Accordingly, the second condition prevents recognition of the contextual keyword var on the grounds that the immediately following input character f is a JavaLetterOrDigit. The sequence v a r f i l e n a m e is therefore tokenized as the identifier varfilename in a local variable declaration.

As another example of the careful recognition of contextual keywords, consider the sequence of 15 input characters n o n - s e a l e d c l a s s. This sequence is usually translated to three tokens - the identifier non, the operator -, and the identifier sealedclass - but in a normal class declaration, where the first condition holds, the first ten input characters are tentatively recognized as the contextual keyword non-sealed. To avoid translating the sequence to two keyword tokens (non-sealed and class) rather than three non-keyword tokens, and to avoid rewarding the programmer for omitting white space before class, the second condition prevents recognition of the contextual keyword. The sequence n o n - s e a l e d c l a s s is therefore tokenized as three tokens in a class declaration.

In the rule above, the first condition depends on details of the syntactic grammar, but a compiler for the Java programming language can implement the rule without fully parsing the input program. For example, a heuristic could be used to track the contextual state of the tokenizer, as long as the heuristic guarantees that valid uses of contextual keywords are tokenized as keywords, and valid uses of identifiers are tokenized as identifiers. Alternatively, a compiler could always tokenize a contextual keyword as an identifier, leaving it to a later phase to recognize special uses of these identifiers.

Chapter 5: Conversions and Contexts

5.5 Casting Contexts

Casting contexts allow the operand of a cast expression (15.16) to be converted to the type explicitly named by the cast operator. Compared to assignment contexts and invocation contexts, casting contexts allow the use of more of the conversions defined in 5.1, and allow more combinations of those conversions.

If the expression is of a primitive type, then a casting context allows the use of one of the following:

If the expression is of a reference type, then a casting context allows the use of one of the following:

If the expression has the null type, then the expression may be cast to any reference type.

If a casting context makes use of a narrowing reference conversion that is checked or partially unchecked (5.1.6.2, 5.1.6.3), then a run time check will be performed on the class of the expression's value, possibly causing a ClassCastException. Otherwise, no run time check is performed.

If an expression can be converted to a reference type by a casting conversion other than a narrowing reference conversion which is unchecked, we say the expression (or its value) is downcast compatible with the reference type.

If an expression of reference type S is downcast compatible with another reference type T, we say that the type S is downcast convertible to type T.

The following tables enumerate which conversions are used in certain casting contexts. Each conversion is signified by a symbol:

In the tables, a comma between symbols indicates that a casting context uses one conversion followed by another. The type Object means any reference type other than the eight wrapper classes Boolean, Byte, Short, Character, Integer, Long, Float, Double.

Table 5.5-A. Casting to primitive types

To
From
byte short char int long float double boolean
byte ω ωη ω ω ω ω -
short η η ω ω ω ω -
char η η ω ω ω ω -
int η η η ω ω ω -
long η η η η ω ω -
float η η η η η ω -
double η η η η η η -
boolean - - - - - - -
Byte ,ω - ,ω ,ω ,ω ,ω -
Short - - ,ω ,ω ,ω ,ω -
Character - - ,ω ,ω ,ω ,ω -
Integer - - - ,ω ,ω ,ω -
Long - - - - ,ω ,ω -
Float - - - - - ,ω -
Double - - - - - - -
Boolean - - - - - - -
Object , , , , , , , ,

Table 5.5-B. Casting to reference types

To
From
Byte Short Character Integer Long Float Double Boolean Object
byte - - - - - - - ,
short - - - - - - - ,
char - - - - - - - ,
int - - - - - - - ,
long - - - - - - - ,
float - - - - - - - ,
double - - - - - - - ,
boolean - - - - - - - ,
Byte - - - - - - -
Short - - - - - - -
Character - - - - - - -
Integer - - - - - - -
Long - - - - - - -
Float - - - - - - -
Double - - - - - - -
Boolean - - - - - - -
Object

Example 5.5-1. Casting for Reference Types

class Point { int x, y; }
interface Colorable { void setColor(int color); }
class ColoredPoint extends Point implements Colorable {
    int color;
    public void setColor(int color) { this.color = color; }
}
final class EndPoint extends Point {}

class Test {
    public static void main(String[] args) {
        Point p = new Point();
        ColoredPoint cp = new ColoredPoint();
        Colorable c;
        // The following may cause errors at run time because
        // we cannot be sure they will succeed; this possibility
        // is suggested by the casts:
        cp = (ColoredPoint)p;  // p might not reference an
                               // object which is a ColoredPoint
                               // or a subclass of ColoredPoint
        c = (Colorable)p;      // p might not be Colorable
        // The following are incorrect at compile time because
        // they can never succeed as explained in the text:
        Long l = (Long)p;            // compile-time error #1
        EndPoint e = new EndPoint();
        c = (Colorable)e;            // compile-time error #2
    }
}

Here, the first compile-time error occurs because the class types Long and Point are unrelated (that is, they are not the same, and neither is a subclass of the other), so a cast between them will always fail.

The second compile-time error occurs because a variable of type EndPoint can never reference a value that implements the interface Colorable. This is because EndPoint is a final type, and a variable of a final type always holds a value of the same run-time type as its compile-time type. Therefore, the run-time type of variable e must be exactly the type EndPoint, and type EndPoint does not implement Colorable.

Example 5.5-2. Casting for Array Types

class Point {
    int x, y;
    Point(int x, int y) { this.x = x; this.y = y; }
    public String toString() { return "("+x+","+y+")"; }
}
interface Colorable { void setColor(int color); }
class ColoredPoint extends Point implements Colorable {
    int color;
    ColoredPoint(int x, int y, int color) {
        super(x, y); setColor(color);
    }
    public void setColor(int color) { this.color = color; }
    public String toString() {
        return super.toString() + "@" + color;
    }
}

class Test {
    public static void main(String[] args) {
        Point[] pa = new ColoredPoint[4];
        pa[0] = new ColoredPoint(2, 2, 12);
        pa[1] = new ColoredPoint(4, 5, 24);
        ColoredPoint[] cpa = (ColoredPoint[])pa;
        System.out.print("cpa: {");
        for (int i = 0; i < cpa.length; i++)
            System.out.print((i == 0 ? " " : ", ") + cpa[i]);
        System.out.println(" }");
    }
}

This program compiles without errors and produces the output:

cpa: { (2,2)@12, (4,5)@24, null, null }

Example 5.5-3. Casting Incompatible Types at Run Time

class Point { int x, y; }
interface Colorable { void setColor(int color); }
class ColoredPoint extends Point implements Colorable {
    int color;
    public void setColor(int color) { this.color = color; }
}

class Test {
    public static void main(String[] args) {
        Point[] pa = new Point[100];

        // The following line will throw a ClassCastException:
        ColoredPoint[] cpa = (ColoredPoint[])pa;
        System.out.println(cpa[0]);
        int[] shortvec = new int[2];
        Object o = shortvec;

        // The following line will throw a ClassCastException:
        Colorable c = (Colorable)o;
        c.setColor(0);
    }
}

This program uses casts to compile, but it throws exceptions at run time, because the types are incompatible.

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 Record Patterns

The following rule applies to a record pattern p:

6.3.4 Scope for Pattern Variables in Switch Labels

Pattern variables can be introduced by patterns that are supported by switch labels and by any when expressions associated with the 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 rules apply to a switch label:

Chapter 13: Binary Compatibility

13.4 Evolution of Classes

13.4.2 sealed, non-sealed, and final Classes

13.4.2.1 sealed Classes

If a class that was freely extensible (8.1.1.2) is changed to be declared sealed, then an IncompatibleClassChangeError is thrown if a binary of a pre-existing subclass of this class is loaded and is not a permitted direct subclass of this class (8.1.6); such a change is not recommended for widely distributed classes.

Changing a class that was declared final to be declared sealed does not break compatibility with pre-existing binaries.

Adding a class to the set of permitted direct subclasses of a sealed class will not break compatibility with pre-existing binaries.

Note that evolving a sealed class by adding a permitted direct subclass is considered a binary compatible change because pre-existing binaries that previously linked without error (e.g., a class file that contains an exhaustive switch (14.11.1)) will continue to link without error. A class file that contains an exhaustive switch will not fail to link if the sealed class that it switches over is expanded by the hierarchy's owner to have a new permitted direct subclass. The JVM is not required to perform exhaustiveness checks when linking a class file that contains an exhaustive switch.

The execution of an exhaustive switch can fail with an error (a MatchException is thrown) if it encounters an instance of a permitted direct subclass that was not known at compile time (14.11.3, 15.28.2). Strictly speaking, the error is not flagging a binary incompatible change of the sealed class, but more accurately a migration incompatible change of the sealed class.

If a class is removed from the set of permitted direct subclasses of a sealed class, then an IncompatibleClassChangeError is thrown if the pre-existing binary of the removed class is loaded.

Deleting the sealed modifier from a class that does not have a sealed direct superclass or a sealed direct superinterface does not break compatibility with pre-existing binaries.

If a sealed class C did have a sealed direct superclass or a sealed direct superinterface, then deleting the sealed modifier would prevent C from being recompiled, as every class with a sealed direct superclass or a sealed direct superinterface must be either final, sealed, or non-sealed.

13.4.26 Evolution of Enum Classes

Adding or reordering enum constants in an enum class will not break compatibility with pre-existing binaries.

As with sealed classes (13.4.2.1), although adding an enum constant to an enum class is considered a binary compatible change, it may cause the execution of an exhaustive switch (14.11.1) to fail with a linkage error (an IncompatibleClassChangeError may be thrown) if the switch encounters the new enum constant that was not known at compile time (14.11.3, 15.28.2).

Deleting an enum constant from an enum class will delete the public field that corresponds to the enum constant (8.9.3). The consequences are specified in 13.4.8. Such a change is not recommended for widely distributed enum classes.

In all other respects, the binary compatibility rules for enum classes are identical to those for normal classes.

13.5 Evolution of Interfaces

13.5.2 sealed and non-sealed Interfaces

If an interface that was freely extensible (9.1.1.4) is changed to be declared sealed, then an IncompatibleClassChangeError is thrown if a binary of a pre-existing subclass or subinterface of this interface is loaded and is not a permitted direct subclass or subinterface of this interface (9.1.4); such a change is not recommended for widely distributed classes.

Adding a class or interface to the set of permitted direct subclasses or subinterfaces, respectively, of a sealed interface will not break compatibility with pre-existing binaries.

As with sealed classes (13.4.2.1), whilst adding a permitted direct subclass or subinterface of a sealed interface is considered a binary compatible change, it may cause the execution of an exhaustive switch (14.11.1) to fail with a linkage error (a MatchException may be thrown) if the switch encounters an instance of the new permitted direct subclass and subinterface that was not known at compile time (14.11.3, 15.28.2).

If a class or interface is removed from the set of permitted direct subclasses or subinterfaces of a sealed interface, then an IncompatibleClassChangeError is thrown if the pre-existing binary of the removed class or interface is loaded.

Changing an interface that was declared sealed to be declared non-sealed does not break compatibility with pre-existing binaries.

A non-sealed interface I must have a sealed direct superinterface. Deleting the non-sealed modifier would prevent I from being recompiled, as every interface with a sealed direct superinterface must be sealed or non-sealed.

Deleting the sealed modifier from an interface that does not have a sealed direct superinterface does not break compatibility with pre-existing binaries.

If a sealed interface I did have a sealed direct superinterface, then deleting the sealed modifier would prevent I from being recompiled, as every interface with a sealed direct superinterface must be sealed or non-sealed.

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 CaseElement {, CaseElement }
default
CaseElement:
CaseConstant
Pattern [ Guard ]
null
default
CaseConstant:
ConditionalExpression
Guard:
when Expression

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. Multiple switch labels are permitted for a switch labeled statement group.

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.

Every switch rule and switch labeled statement group starts with a switch label. A switch label consists of one or more case and default labels. A case label has one or more case elements. A switch label is said to support a case element if it has a case label that has that case element.

A pattern case element may have an associated when expression. A pattern case element is said to be unguarded if either (i) it has no associated when expression, or (ii) it has an associated when expression that is a constant expression (15.29) with value true. A case element is unguarded if it is not a pattern case element or it is an unguarded pattern case element.

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 complexity is due to the historical support of two ways of combining case label elements in switch labels for statement groups (for example case 1: case 2 and case 1,2).

Any when expression associated with a pattern case element must have type boolean or Boolean. Any variable that is used but not declared in a when expression must be either final or effectively final (4.12.4). It is a compile-time error if a when expression is a constant expression (15.29) with the value false.

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 -> ....

A switch label is said to be a default switch label if either it has a default label, or it supports a default case element. It is a compile-time error if a switch block has more than one default switch label.

The switch block of a switch statement or a switch expression is compatible with the type of the selector expression, T, if both of the following are true:

The switch block of a switch statement or a switch expression is switch compatible with the type of the selector expression, T, if every switch label in the switch block is switch compatible with T. A switch label is switch compatible with T if every case element it supports, if any, is switch compatible with T, which is defined as follows:

In particular, a switch block containing just a default label is compatible with all selector expressions.

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

A switch label is said to dominate another switch label if the former applies to every value that the latter applies to. It is a compile-time error if a switch label in a switch block dominates any switch label that follows it. The rules for determining dominance are as follows:

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 introduces a pattern variable.

  2. There is a statement preceding it in the switch block and that statement can complete 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 supporting 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
    ... 
}

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 supporting a pattern p in the switch block where p is unconditional at the type of the selector expression (14.30.3).

A pattern that is unconditional at the type of the selector expression will, as the name suggests, match every value and so behaves much like a default switch label.

14.11.1.1 Exhaustive Switch Blocks

The switch block of a switch expression or switch statement is exhaustive for a selector expression of type T if either (i) the switch block contains a default switch label, or (ii) the set containing all the unguarded case elements supported by switch labels in the switch block is exhaustive for T, which is specified as follows:

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

14.11.1.2 Executable Switch Blocks and Determining which Switch Label Applies at Run-Time

As the meaning of some patterns is determined by the type of the expression that are being matching against, patterns must be resolved before pattern matching can be performed (14.30.2). Consequently, any patterns supported by switch labels in a switch expression or switch statement must also be resolved.

An executable switch expression or switch statement is one where every default and case label has been transformed with respect to the type of the selector expression, T, as follows:

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

A case switch label can contain support several case constants. 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 supports a pattern 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 supports a pattern or a null.

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 19, switch statements (and switch expressions) were limited in two ways: (i) the type of the selector expression was restricted to an integral type (excluding long), an enum type, or String; and (ii) only case constant elements were supported. However, 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 19, 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 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.

14.11.3 Execution of a switch Statement

An executable A switch statement (14.11.1.2) 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.2). 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 may 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 variables, if any, 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

A type pattern is used to test whether a value is an instance of the type appearing in the pattern. A pattern may be parenthesized to assist in readability. A record pattern is used to test whether a value is an instance of a record class type and, if it is, to recursively perform pattern matching on the record component values.

Pattern:
TypePattern
ParenthesizedPattern
RecordPattern
TypePattern:
LocalVariableDeclaration
ParenthesizedPattern:
( Pattern )
RecordPattern:
ReferenceType RecordStructurePattern [ Identifier ]
RecordStructurePattern:
( [ RecordComponentPatternList ] )
RecordComponentPatternList :
Pattern { , Pattern }

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.

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.

A type pattern that does not appear as an element in a record component pattern list is called a top-level type pattern.

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 declared type of a pattern variable declared in a top-level type pattern is the reference type denoted by LocalVariableType.

The declared type of a pattern variable declared in a type pattern that is not a top-level type pattern is determined as follows:

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.

A record pattern consists of a ReferenceType, a record component pattern list, and an optional Identifier. If ReferenceType is not a record class type (8.10) then a compile-time error occurs.

A record pattern with an Identifier is called a named record pattern.

The length of record component pattern list must be the same as the length of the record component list in the declaration of the record class named by ReferenceType; otherwise a compile-time error occurs.

Currently, there is no support for variable arity record patterns. This may be supported in future versions of the Java Programming Language.

A record pattern declares the local variables, if any, that are declared by the patterns in the record component pattern list.

A named record pattern additionally declares a local variable, called the record pattern variable, which is named Identifier and whose declared type is the type denoted by ReferenceType.

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

Currently, no syntax exists for any patterns so they can not be used as a pattern in a pattern instanceof expression, or as a pattern in a switch label of a switch expression or switch statement. It is possible that future versions of the Java programming language may relax this restriction.

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

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

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

Compatibility of an expression with a pattern is used by the instanceof pattern match operator (15.20.2).

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 will initialize all the pattern variables declared by the pattern, if any.

Before pattern matching is performed, all patterns are first resolved with respect to the type of the expression that they are to be matched against (either the selector expression of a switch statement or switch statement, or the RelationalExpression of an instanceof expression), resulting in a possibly amended pattern.

Resolving a pattern at type U is specified as follows:

This process of resolving a pattern is to capture the correct semantics of record patterns. Consider, for example:

class Super {}
class Sub extends Super {}
record R(Super s) {}

We expect all non-null values of type R to match the pattern R(Super s), including the value resulting from evaluating the expression new R(null). (Even though the null value does not match the pattern Super s.) However, we would not expect this value to match the pattern R(Sub s) as the null value for the record component does not match the pattern Sub s.

The meaning of a pattern occurring in a record component pattern list is then determined with respect to the record declaration. Resolution replaces any type patterns appearing in a record component pattern list that should match all values including null with instances of the special any pattern. In our example above, the pattern R(Sub s) is resolved to the pattern R(Sub s), whereas the pattern R(Super s) is resolved to a record pattern with type R and a record component pattern list containing an any pattern.

The process of pattern matching may involve expression evaluation or statement execution. Accordingly, pattern matching is said to complete abruptly if evaluation of a expression or execution of a statement 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 Properties of Patterns

A pattern p is said to be applicable at a type T if one of the following rules apply:

A pattern p is said to be unconditional at a type T if every value of type T will match p (after p has been resolved at type T (14.30.2)), and is defined as follows:

Note that record patterns are not unconditional at any type because the null reference does not match any record pattern.

A pattern p is said to dominate another 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

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, 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 CaseElement {, CaseElement }
default
CaseElement:
CaseConstant
Pattern { Guard }
null
default
CaseConstant:
ConditionalExpression
Guard:
when Expression

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.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

An executable A switch expression (14.11.1.2) 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.2). 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