Primitive types in patterns, instanceof, and switch (Preview)

Changes to the Java® Language Specification • Version 22-internal-adhoc.abimpoudis.20231213

This document describes changes to the Java Language Specification to support primitive types to pattern matching of instanceof, which is a preview feature of Java SE 23. See JEP:455 for 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:

2023-12-13: Streamlining of exact testing conversions in 5.7.1 and 5.7.2.

2023-12-11: Update on top of JLS 22 (inclusion of unnamed variables & patterns, and testing contexts in 5.7).

2023-12-07: Introduce exactly promoted type.

2023-11-17: Use a wider format W for the exactness definition.

2023-10-30: Simplifications: exactness decoupled from unconditional exactness, instanceof​ as a type comparison operator streamlined, property of unconditional.

2023-10-10: Editorial changes and added JEP number.

2023-09-13: Update draft spec on top of JLS 21. Move exactness under casting context in 5.5.

2023-01-25: First draft. Add definition of exact representation conversions in 5.1.13. Update section about instanceof, pattern properties and switch.

All the cross-references will be updated to point to JLS 22 when it is released.

Chapter 5: Conversions and Contexts

Every expression written in the Java programming language either produces no result (15.1) or has a type that can be deduced at compile time (15.3). When an expression appears in most contexts, it must be compatible with a type expected in that context; this type is called the target type. For convenience, compatibility of an expression with its surrounding context is facilitated in two ways:

If neither strategy is able to produce the appropriate type, a compile-time error occurs.

The rules determining whether an expression is a poly expression, and if so, its type and compatibility in a particular context, vary depending on the kind of context and the form of the expression. In addition to influencing the type of the expression, the target type may in some cases influence the run time behavior of the expression in order to produce a value of the appropriate type.

Similarly, the rules determining whether a target type allows an implicit conversion vary depending on the kind of context, the type of the expression, and, in one special case, the value of a constant expression (15.29). A conversion from type S to type T allows an expression of type S to be treated at compile time as if it had type T instead. In some cases this will require a corresponding action at run time to check the validity of the conversion or to translate the run-time value of the expression into a form appropriate for the new type T.

Example 5.0-1. Conversions at Compile Time and Run Time

The conversions possible in the Java programming language are grouped into several broad categories:

There are seven kinds of conversion contexts in which poly expressions may be influenced by context or implicit conversions may occur. Each kind of context has different rules for poly expression typing and allows conversions in some of the categories above but not others. The contexts are:

The term "conversion" is also used to describe, without being specific, any conversions allowed in a particular context. For example, we say that an expression that is the initializer of a local variable is subject to "assignment conversion", meaning that a specific conversion will be implicitly chosen for that expression according to the rules for the assignment context. As another example, we say that an expression undergoes "casting conversion" to mean that the expression's type will be converted as permitted in a casting context.

Example 5.0-2. Conversions In Various Contexts

class Test {
    public static void main(String[] args) {
        // Casting conversion (5.5) of a float literal to
        // type int. Without the cast operator, this would
        // be a compile-time error, because this is a
        // narrowing conversion (5.1.3):
        int i = (int)12.5f;

        // String conversion (5.4) of i's int value:
        System.out.println("(int)12.5f==" + i);

        // Assignment conversion (5.2) of i's value to type
        // float. This is a widening conversion (5.1.2):
        float f = i;

        // String conversion of f's float value:
        System.out.println("after float widening: " + f);

        // Numeric promotion (5.6) of i's value to type
        // float. This is a binary numeric promotion.
        // After promotion, the operation is float*float:
        System.out.print(f);
        f = f * i;

        // Two string conversions of i and f:
        System.out.println("*" + i + "==" + f);

        // Invocation conversion (5.3) of f's value
        // to type double, needed because the method Math.sin
        // accepts only a double argument:
        double d = Math.sin(f);

        // Two string conversions of f and d:
        System.out.println("Math.sin(" + f + ")==" + d);
    }
}

This program produces the output:

(int)12.5f==12
after float widening: 12.0
12.0*12==144.0
Math.sin(144.0)==-0.49102159389846934

5.1 Kinds of Conversion

5.1.2 Widening Primitive Conversion

19 specific conversions on primitive types are called the widening primitive conversions:

A widening primitive conversion that does not lose information about the overall magnitude of a numeric value in the following cases, where the numeric value is preserved exactlyis called an exact widening primitive conversion and the numeric value is preserved exactly. Such a conversion can be one of the following:

A widening primitive conversion from int to float, or from long to float, or from long to double, may result in loss of precision, that is, the result may lose some of the least significant bits of the value. In this case, the resulting floating-point value will be a correctly rounded version of the integer value, using the round to nearest rounding policy (15.4).

A widening conversion of a signed integer value to an integral type T simply sign-extends the two's-complement representation of the integer value to fill the wider format.

A widening conversion of a char to an integral type T zero-extends the representation of the char value to fill the wider format.

A widening conversion from int to float, or from long to float, or from int to double, or from long to double occurs as determined by the rules of IEEE 754 for converting from an integer format to a binary floating-point format.

A widening conversion from float to double occurs as determined by the rules of IEEE 754 for converting between binary floating-point formats.

Despite the fact that loss of precision may occur, a widening primitive conversion never results in a run-time exception (11.1.1).

Example 5.1.2-1. Widening Primitive Conversion

class Test {
    public static void main(String[] args) {
        int big = 1234567890;
        float approx = big;
        System.out.println(big - (int)approx);
    }
}

This program prints:

-46

thus indicating that information was lost during the conversion from type int to type float because values of type float are not precise to nine significant digits.

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 performedthe conversion performs a validity check at run time on the class of the expression's value, possibly causing a ClassCastException. Otherwise, no run time check is performedno validity check is performed at run time, although other actions may be performed at run time.

5.7 Testing Contexts

Testing contexts arise for expressions when a value of one type is to be compared and possibly converted to another type. Testing contexts allow the operand of a type comparison operator (15.20.2) to be compared to another type. Testing contexts also allow the operand of a pattern match operator (15.20.2), or the selector expression of a switch expression or statement that has at least one pattern case label associated with its switch block (14.11.1) to be compared and converted to a type as part of the process of pattern matching. As pattern matching is an inherently conditional process (14.30.2), it is expected that a testing context will make use of conversions that may fail or lose information at run time.

Testing contexts use similar conversions for reference types as casting contexts except that they do not permit narrowing reference conversions that are unchecked (5.1.6.2).

If the expression is of a primitive type, then a testing context allows the use of an identity conversion (5.1.1).one of the following:

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

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

If a testing context makes use of a narrowing reference conversion, then a run time check will be performed on the class of the expression's value, possibly causing a ClassCastException.

Whether there is a testing conversion from type S to type T is distinct from whether a value of type S can be converted to type T without loss of information. For example, there is a testing conversion from int to byte, and from Object to String, but there are many int values that cannot be represented as a byte, and there are many Object values that do not refer to instances of String. The run-time process of pattern matching is sensitive to whether loss of information occurs, so it relies on the notion of a testing conversion being exact for a given value.

5.7.1 Exact Testing Conversions

A testing conversion of a value is exact if it yields a result without loss of information or throwing an exception. Otherwise, it is inexact.

Loss of information can occur during either a widening primitive conversion that is not exact (5.1.2), or a narrowing primitive conversion (5.1.3), or a widening and narrowing primitive conversion (5.1.4). The loss can take one of more of the following forms:

Applying one of these conversions may lead to loss of information that in turn is a potential source of bugs. For example, if the int variable i stores the value 1000 then a narrowing primitive conversion to byte will yield the result -24. Loss of information has occurred: both the magnitude and the sign of the result are different than those of the original value. As such, a conversion from int to byte for the value 1000 is inexact. In contrast, a conversion from int to byte for the value 10 is exact because the result, 10, is the same as the original value.

An exception can occur during either a narrowing reference conversion that is checked (5.1.6.2), or an unboxing conversion (5.1.8).

A run time check is needed to determine whether a conversion causes loss of information or throws an exception. If the check determines that no loss of information occurs, or no exception is thrown, then the conversion is exact. Otherwise the conversion is inexact, and its result or exception is discarded as if the conversion had never occurred.

If a testing conversion consists of more than one conversion, then if all such conversions are exact then the testing conversion is exact; otherwise the testing conversion is inexact.

The run time check to determine whether the conversion of a value from a primitive type S to a primitive type T loses information is non-obvious. The obvious way to check for loss of information would be to convert the value from S to T, then convert the result from T back to S, then compare that final result with the original value. However, this "round trip" conversion from S to T and back to S would be misleading because it would be possible for the final result to equal the original value even if the original conversion of the value from S to T was inexact.

As an example, consider the conversion of Integer.MAX_VALUE from int to float, with result y. This conversion is inexact due to the round to nearest rounding policy: loss of precision occurs (15.4). Converting y back to int, with result z, is also inexact due to the round toward zero rounding policy (4.2.4). In this case, z is equal to the original value Integer.MAX_VALUE, but it would be erroneous to conclude that the original conversion of Integer.MAX_VALUE from int to float preserved all information.

The non-obvious but safe way to check for loss of information is to convert the value from S to T, then promote both the result and the original value to a type capable of representing all the values of S and T. This type is known as the exactly promoted type. If the original conversion from S to T lost information then the two promoted values will compare as unequal, revealing the loss of information. There is no possibility of the original conversion's inexactness being cancelled out by the inexactness of another conversion, as can happen with a "round trip".

For a testing conversion from a primitive type S to a primitive type T, the exactly promoted type is determined as follows:

Let C be a testing conversion of a value v from a primitive type S to a primitive type T. Then one of the following holds:

5.7.2 Unconditionally Exact Testing Conversions

Most conversions allowed in a testing context are exact or inexact depending on the value that is converted at run time. However, some conversions are always exact regardless of the value. These conversions are said to be unconditionally exact. It is known at compile time that an unconditionally exact conversion will yield a result at run time without loss of information or throwing an exception. The unconditionally exact conversions are:

For example, a widening primitive conversion from byte to int is unconditionally exact because it will always succeed with no loss of information about the magnitude of the numeric value.

Some of these conversions require action at run time, and in the case of a boxing conversion (5.1.7) may even fail with an OutOfMemoryError. However, the run-time behavior does not affect whether the conversion is unconditionally exact.

Unconditional exactness is used at compile time to check the dominance of one pattern over another in a switch block (14.11.1), and whether a switch block as a whole is exhaustive (14.11.1.1).

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, or a reference type, or a compile-time error occursmay be any type.

The types allowed for a selector expression have been expanded over the years. Java SE 1.0 supported byte, short, char, int selector types. In Java SE 5.0 autoboxing was introduced, so switch was expanded to support the wrapper classes Byte, Short, Character, Integer, alongside enumerations. In Java SE 7, String was added. In Java SE 21, reference types were added (preserving the existing support for primitives) to account for pattern matching with type patterns in case labels. Java SE 23 supports all primitive types and their boxes, extending support with case constants to allow longs, floats, etc.; lifting all restrictions on the values that can be compared and pattern matched via switch and instanceof (15.20.2) allows performing data exploration uniformly.

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}
case null [, default]
case CasePattern {, CasePattern} [Guard]
default
CaseConstant:
ConditionalExpression
CasePattern:
Pattern
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 either a (non-empty) list of case constants, a null literal, or a (non-empty) list of case patterns.

Every case constant must be either a constant expression (15.29), or the name of an enum constant (8.9.1), otherwise a compile-time error occurs.

A case label with a null literal may have an optional default.

A case label with case patterns may have an optional when expression, known as a guard, which represents a further test on values that match the patterns. A case label is said to be unguarded if either (i) it has no guard, or (ii) it has a guard that is a constant expression (15.29) with value true; and guarded otherwise.

It is a compile-time error for a case label to have more than one case pattern and declare any pattern variables (other than those declared by a guard associated with the case label).

If a case label with more than one case pattern could declare pattern variables, then it would not be clear which variables would be initialized if the case label were to apply. For example:

Object obj = ...;
switch (obj) {
  case Integer i, Boolean b -> {
    ...       // Error! Is i or b initialized?
  }
  ...
}

Even if only one of the case patterns declares a pattern variable, it would still not be clear whether the variable was initialized or not; for example:

Object obj = ...;
switch (obj) {
  case Integer i, Boolean _ -> {
    ...       // Error! Is i initialized?
  }
  ...
}

The following does not result in a compile-time error:

Object obj = ...;
switch (obj) {
  case Integer _, Boolean _ -> {
    ...       // Matches both an Integer and a Boolean
  }
  ...
}

Switch labels and their case constants, null literals, and case patterns are said to be associated with the switch block.

For a given switch block bothall of the following must be true, otherwise a compile-time error occurs:

A guard associated with a case label must satisfy all of the following conditions, otherwise a compile-time error occurs:

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

Note that case constants must be assignment compatible with the selector expression, while case patterns allow values to be compared and converted in a testing context (5.7, 14.30.3).

Prior to Java SE 23, case constants were limited to the types given in the first item: char, byte, short, etc. Java SE 23 expanded the set of valid case constants to allow longs, floats, etc.

Note that a selector expression of type long, float, or double requires case constants that are, respectively, integer literals of type long (5L), floating-point literals of type float (5f), and floating-point literals of type double (5d). Intermixing floating-point selector types with integral literals is not allowed. For example a widening primitive conversion from int to float is lossy. While, a widening primitive conversion from int to double is unconditionally exact, integer literals are disallowed since mixing implicit and explicit conversions in case literals could result in incorrect code in case of complex constant expressions.

Switch blocks are not designed to work with the types boolean, long, float, and double. The selector expression of a switch statement or switch expression can not have one of these types.

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.

If the switch block of a switch statement or switch expression whose selector expression is of type boolean or Boolean has one case constant associated with the switch block that names true, one that names false, and a default label, then a compile-time error occurs.

A switch label in a switch block is said to be dominated if for every value that it applies to, it can be determined that one of the preceding switch labels would also apply. It is a compile-time error if any switch label in a switch block is dominated. The rules for determining whether a switch label is dominated are as follows:

It is a compile-time error if there is a case label with n (n>1) case patterns p1, ..., pn in a switch block where one of the patterns pi (1≤i<n) dominates another of the patterns pj (i<j≤n).

It is a compile-time error if any of the following holds:

If used, a default label should come last in a switch block.

For compatibility reasons, a default label may appear before case labels that do not have a null literal or case patterns.

int i = ...;
switch(i) {
    default ->
        System.out.println("Some other integer");
    case 42 -> // allowed
        System.out.println("42");
}

If used, a case null, default label should come last in a switch block.

It is a compile-time error if, in a switch block that consists of switch labeled statement groups, a statement is labeled with a case label that declares one or more pattern variables ([6.3.3]), and either:

The first condition prevents a statement group from "falling through" to another statement group without initializing pattern variables. For example, were the statement labeled by case Integer i reachable from the preceding statement group, the pattern variable i would not have been initialized:

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

Switch blocks consisting of switch label statement groups allow multiple labels to apply to a statement group. The second condition prevents a statement group from being executed based on one label without initializing the pattern variables of another label. For example:

Object o = "Hello World";
switch (o) {
    case String s:
    case Integer i:
        System.out.println(i + 1);  // Error! Can be reached
                                    // without matching the
                                    // pattern `Integer i`
    default:
}
Object obj = null;
switch (obj) {
    case null:
    case String s:
        System.out.println(s);      // Error! Can be reached
                                    // without matching the
                                    // pattern `String s`
    default:
}

Both of these conditions apply only when the case pattern declares pattern variables. The following examples, in contrast, are unproblematic:

record R() {}
record S() {}
Object o = "Hello World";
switch (o) {
    case String s:
        System.out.println(s);      // No break
    case R():                       // No pattern variables declared
        System.out.println("It's either an R or a string");
        break;
    default:
}
Object ob = new R();
switch (ob) {
    case R():
    case S():                       // Multiple case labels
        System.out.println("Either R or an S");
        break;
    default:
}
Object obj = null;
switch (obj) {
    case null:
    case R():                       // Multiple case labels
        System.out.println("Either null or an R");
        break;
    default:
}
14.11.1.1 Exhaustive Switch Blocks

The switch block of a switch expression or switch statement is exhaustive for a selector expression e if one of the following cases applies:

A set of case elements, PCE, covers a type T if one of the following cases applies:

Ordinarily record patterns match only a subset of the values of the record type. However, a number of record patterns in a switch block can combine to actually match all of the values of the record type. For example:

sealed interface I permits A, B, C {}
final class A   implements I {}
final class B   implements I {}
record C(int j) implements I {}  // Implicitly final
record Box(I i) {}

int testExhaustiveRecordPatterns(Box b) {
    return switch (b) {     // Exhaustive!
        case Box(A a) -> 0;
        case Box(B b) -> 1;
        case Box(C c) -> 2;
    };
}

Determining whether this switch block is exhaustive requires the analysis of the combination of the record patterns. The set containing the record pattern Box(I i) covers the type Box, and so the set containing the patterns Box(A a), Box(B b), and Box(C c) can be rewritten to the set containing the pattern Box(I i). This is because the set containing the patterns A a, B b, C c reduces to the pattern I i (because the same set covers the type I), and thus the set containing the patterns Box(A a), Box(B b), Box(C c) reduces to the pattern Box(I i).

However, rewriting a set of record patterns is not always so simple. For example:

record IPair(I i, I j){}

int testNonExhaustiveRecordPatterns(IPair p) {
    return switch (p) {     // Not Exhaustive!
        case IPair(A a, A a) -> 0;
        case IPair(B b, B b) -> 1;
        case IPair(C c, C c) -> 2;
    };
}

It is tempting to apply the logic from the previous example to rewrite the set containing the patterns IPair(A a, A a), IPair(B b, B b), IPair(C c, C c) to the set containing the pattern IPair(I i, I j), and hence conclude that the switch block exhausts the type IPair. But this is incorrect as, for example, the switch block does not actually have a label that matches an IPair value whose first component is an A value, and second component is a B value. It is only valid to combine record patterns on one component if they match the same values in the other components. For example, the set containing the three record patterns IPair(A a, I i), IPair(B b, I i), and IPair(C c, I i) can be reduced to the pattern IPair(I j, I i).

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

14.11.1.2 Determining which Switch Label Applies at Run Time

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 associated with the switch block applies to the value of the selector expression. This proceeds as follows:

  1. If T is a reference type and if Ifthe value is the null reference, then a case label with a null literal applies.

  2. If the value is not the null reference, thenThen we determine the first (if any) case label in the switch block that applies to the value as follows:

    • If T is a reference type and if the value is not the null reference, then, a A case label with a case constant c applies to a value of type Character, Byte, Short, or Integer , Long, Float, Double or Boolean, if the value is first subjected to unboxing conversion (5.1.8) and the constant c is equal to the unboxed value.

      Any unboxing conversion will complete normally as the value being unboxed is guaranteed not to be the null reference.

      Equality is defined in terms of the == operator (15.21).

    • If T is a primitive type, then a A case label with a case constant c applies to a value that is of type char, byte, short, int, or long, float, double, boolean, or String or an enum type if the constant c is equal to the value.

      Equality is defined in terms of the == operator (15.21) for the integral types and the boolean type, and in terms of representation equivalence (java.lang.Double) for the floating-point types. unless If the value is a String, in which case equality is defined in terms of the equals method of class String.

    • Determining that a case label with a case pattern p applies to a value proceeds first by checking if the value matches the pattern p (14.30.2).

      If pattern matching completes abruptly then the process of determining which switch label applies completes abruptly for the same reason.

      If pattern matching succeeds and the case label is unguarded then this case label applies.

      If pattern matching succeeds and the case label is guarded, then the guard is evaluated. If the result is of type Boolean, it is subjected to unboxing conversion (5.1.8).

      If evaluation of the guard or the subsequent unboxing conversion (if any) completes abruptly for some reason, the process of determining which switch label applies completes abruptly for the same reason.

      Otherwise, if the resulting value is true then the case label applies.

    • Determining that a case label with case patterns p1, ..., pn (n≥1) applies to a value proceeds by finding the first (if any) case pattern pi (1≤i≤n) that applies to the value.

      Determining that a case pattern applies to a value proceeds first by checking the value matches the pattern (14.30.2). Then:

      • If pattern matching completes abruptly then the whole process of determining which switch label applies completes abruptly for the same reason.

      • If pattern matching succeeds and the case label is unguarded then this case pattern applies.

      • If pattern matching succeeds and the case label is guarded, then the guard is evaluated. If the result is of type Boolean, it is subjected to unboxing conversion (5.1.8).

        If evaluation of the guard or the subsequent unboxing conversion (if any) completes abruptly for some reason, then the whole process of determining which switch label applies completes abruptly for the same reason.

        Otherwise, if the resulting value is true then the case pattern applies.

    • A case null, default label applies to every value.

  3. If the value is not the null reference, when T is a reference type, and no case label applies according to the rules of step 2, but there is a default label associated with the switch block, then the default label applies.

A single case label can contain several case constants. The label applies to the value of the selector expression if any one of its constants is equal to the value of the selector expression. For example, in the following code, the case label applies 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 case label with a case 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.

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) there is a case pattern or null literal associated with the switch block.

Prior to Java SE 23 only char, byte, short, int and reference types where supported as the possible selector type of the switch. Since this restriction is lifted in Java SE 23, switch statements with the selector types of float, double, long, boolean or one of their wrapper types are also considered as enhanced.

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 21, switch statements (and switch expressions) were limited in two ways: (i) the type of the selector expression was restricted to either an integral type (excluding long), an enum type, or String and (ii) no case null labels 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!
}

In Java SE 21, in addition to supporting case patterns, the two limitations of switch statements (and switch expressions) listed above were relaxed to (i) allow a selector expression of any reference type, and (ii) to allow a case label with a null literal. The designers of the Java programming language also 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 enhanced switch statements are not required to be exhaustive.

14.30 Patterns

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 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
RecordPattern
TypePattern:
LocalVariableDeclaration
RecordPattern:
ReferenceType ( [ComponentPatternList] )
ComponentPatternList:
ComponentPattern {, ComponentPattern }
ComponentPattern:
Pattern
MatchAllPattern
MatchAllPattern:
_

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 pattern is nested in a record pattern if (1) it appears directly in the component pattern list of the record pattern, or (2) it is nested in a record pattern that appears directly in the component pattern list of the record pattern. A pattern is top level if it is not nested in a record pattern.

A type pattern declares one local variable, known as a pattern variable. If the declaration includes an identifier then this specifies the name of the pattern variable, otherwise the pattern variable is called an unnamed 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 declared in a top level type pattern is the reference type denoted by LocalVariableType.

The type of a pattern variable declared in a nested type pattern is determined as follows:

A type pattern is said to be null matching if it is appears directly in the component pattern list of a record pattern with type R, where the corresponding record component of R has type U, U is a reference type and the type pattern is unconditional for the type U (14.30.3).

Note that this compile-time property of type patterns is used in the run-time process of pattern matching (14.30.2), so it is associated with the type pattern for use at run time.

A record pattern consists of a ReferenceType and a component pattern list containing component patterns, if any. If ReferenceType is not a record class type (8.10) then a compile-time error occurs.

If the ReferenceType is a raw type, then the type of the record pattern is inferred, as described in 18.5.5. It is a compile-time error if no type can be inferred for the record pattern.

If the ReferenceType (or any part of it) is annotated then a compile-time error occurs.

Future versions of the Java Programming Language may lift this restriction on annotations.

Otherwise, the type of the record pattern is ReferenceType.

The length of the record pattern's 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.

A record pattern does not directly declare any pattern variables itself, but may contain declarations of pattern variables in the component pattern list.

It is a compile-time error if a record pattern contains more than one declaration of a pattern variable with the same name.

The match-all pattern is a special pattern that declares no pattern variables and can only appear directly in the component pattern list of a record pattern r.

Let R be the type of the record pattern r, and let T be the type of the corresponding component field in R (8.10.3). The type of the match-all pattern is the upward projection of T with respect to all synthetic type variables mentioned by T.

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.

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

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 for a type T if every value of type T will matchmatches p, and so the testing aspect of pattern matching could be elidedso that pattern matching requires no action at run time. It is defined as follows:

Note that no record pattern is unconditional 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.5 Expressions and Run-Time Checks

If the type of an expression is a primitive type, then the value of the expression is of that same primitive type.

If the type of an expression is a reference type, then the class of the referenced object, or even whether the value is a reference to an object rather than null, is not necessarily known at compile time.

There are a few places in the Java programming language where the actual class of a referenced object or value of a primitive type affects program execution in a manner that cannot be deduced from the type of the expression. They are as follows:

Situations where the class of an object is not statically known may lead to run-time type errors.

In addition, there are situations where the statically known type may not be accurate at run time. Such situations can arise in a program that gives rise to compile-time unchecked warnings. Such warnings are given in response to operations that cannot be statically guaranteed to be safe, and cannot immediately be subjected to dynamic checking because they involve non-reifiable types (4.7). As a result, dynamic checks later in the course of program execution may detect inconsistencies and result in run-time type errors.

A run-time type error can occur only in these situations:

15.16 Cast Expressions

A cast expression converts, at run time, a value of one numeric type to a similar value of another numeric type; or confirms, at compile time, that the type of an expression is boolean; or checks, at run time, that a reference value refers to an object either whose class is compatible with a specified reference type or list of reference types, or which embodies a value of a primitive type.

CastExpression:
( PrimitiveType ) UnaryExpression
( ReferenceType {AdditionalBound} ) UnaryExpressionNotPlusMinus
( ReferenceType {AdditionalBound} ) LambdaExpression #

The following production from [4.4] is shown here for convenience:

AdditionalBound:
& InterfaceType

The parentheses and the type or list of types they contain are sometimes called the cast operator.

If the cast operator contains a list of types, that is, a ReferenceType followed by one or more AdditionalBound terms, then all of the following must be true, or a compile-time error occurs:

The target type for the casting context (5.5) introduced by the cast expression is either the PrimitiveType or the ReferenceType (if not followed by AdditionalBound terms) appearing in the cast operator, or the intersection type denoted by the ReferenceType and AdditionalBound terms appearing in the cast operator.

The type of a cast expression is the result of applying capture conversion (5.1.10) to this target type.

Casts can be used to explicitly "tag" a lambda expression or a method reference expression with a particular target type. To provide an appropriate degree of flexibility, the target type may be a list of types denoting an intersection type, provided the intersection induces a functional interface (9.8).

The result of a cast expression is not a variable, but a value, even if the result of evaluating the operand expression is a variable.

If the compile-time type of the operand cannot be converted by casting conversion (5.5) to the target type specified by the cast operator, then a compile-time error occurs.

Otherwise, at run time, the operand value is converted (if necessary) by casting conversion to the target type specified by the cast operator.

A ClassCastException is thrown if a cast is found at run time to be impermissible.

Some casts result in an error at compile time. Some casts can be proven, at compile time, always to be correct at run time. For example, it is always correct to convert a value of a class type to the type of its superclass; such a cast should require no special action at run time. Finally, some casts cannot be proven to be either always correct or always incorrect at compile time. Such casts require a test at run time. See 5.5 for details.

When the result of a cast expression is the same numerical value exactly or no ClassCastException was throwed, the conversion is characterized as exact (5.5.1).

15.20 Relational Operators

15.20.2 The instanceof Operator

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

InstanceofExpression:
RelationalExpression instanceof ReferenceTypeType
RelationalExpression instanceof Pattern

If the operand to the right of the instanceof keyword is a ReferenceTypeType, 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 type of the expression RelationalExpression can be a reference type, a primitive type or the null type.

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, or a reference type, or a compile-time error occursmay be any type.

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

Chapter 19: Syntax

This chapter repeats the syntactic grammar given in Chapters 4, 6-10, 14, and 15, as well as key parts of the lexical grammar from Chapter 3, using the notation from 2.4.

The production for InstanceofExpression is the only one that changes. The rest of this section is unchanged.

Productions from 15

InstanceofExpression:
RelationalExpression instanceof ReferenceTypeType
RelationalExpression instanceof Pattern