Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8213076

Pattern Matching for switch (Preview)

    Details

    • Type: JEP
    • Status: Draft
    • Priority: P3
    • Resolution: Unresolved
    • Fix Version/s: None
    • Component/s: specification
    • Labels:
      None
    • Author:
      Gavin Bierman
    • JEP Type:
      Feature
    • Exposure:
      Open
    • Subcomponent:
    • Scope:
      SE
    • Discussion:
      amber dash dev at openjdk dot java dot net

      Description

      Summary

      Enhance the Java programming language with pattern matching for switch expressions and statements, along with extensions to the language of patterns. Pattern matching was added to instanceof in Java SE 16; extending pattern matching to switch allows a target expression to be tested against a number of patterns, each with a specific action, allowing complex data-oriented queries to be expressed concisely and safely.

      Goals

      • Expand the expressiveness and applicability of switch expressions and statements by allowing patterns to appear in switch labels.

      • Introduce a new pattern, the guard pattern, to allow pattern matching logic to be refined with arbitrary boolean expressions.

      • Introduce a conditional-and operator on patterns, to allow two patterns to be combined into a single pattern.

      • Allow the historical null-hostility of switch to be relaxed where desired.

      • All existing switch expressions and statements will continue to compile without any changes, and execute with identical semantics.

      Non-goals

      • To keep current switch and introduce a new, separate, switch-like statement/expression with pattern-matching semantics.

      • To make the behavior of pattern switch completely different from that of current switch.

      Motivation

      JEP 394 extended the instanceof operator to take a type pattern operand. This allows familiar instanceof-and-cast code such as the following:

      if (obj instanceof String) {
          String s = (String) obj;
          // use s
      }

      to be simplified to the following:

      if (obj instanceof String s) {
          // use s
      }

      It is not uncommon to compare a single variable against multiple possible alternatives; Java supports this with switch statements and, since JEP 361, switch expressions . But switch is unfortunately very limited; you can only switch on values of a few types -- numeric types, enum types and String -- and you can only test for exact equality against constants. We might like to use patterns to test the same expression against a number of possibilities, taking specific action on each, but as the existing switch statement doesn't support that, we end up with a chain of if...else tests such as the following:

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

      The above code benefits from using pattern instanceof expressions, but it's clearly not perfect. Most importantly the approach allows coding errors to remain hidden -- because we've used an overly-general control construct. The intent of the above code is to assign something to formatted in each arm of the if...else chain. But, there is nothing here that enables the compiler to verify this invariant. If some block -- perhaps one that is executed rarely in practice -- forgets to assign to formatted, we have a bug. (Leaving formatted as a blank local or blank final would at least enlist the "definite assignment" analysis in this effort, but this is not always done.) Finally, the above code is less optimizable; absent compiler heroics, it will have O(n) time complexity, even though the underlying problem is often O(1).

      But switch is a perfect "match" for pattern matching! If we extend switch statements and expressions to work on any type, and allow case labels to use patterns, we could rewrite the above method more clearly and reliably using such a pattern-enhanced switch expression:

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

      The semantics of such a switch is clear: a case label with a pattern matches the value of the selector expression if the value matches the pattern.

      Now, the intent of the code is far clearer, because we're using the right control construct -- we're saying "the parameter o matches at most one of the following conditions, figure it out and evaluate the corresponding arm". As a bonus, it is more optimizable too; in this case we are more likely to be able to do the dispatch in O(1) time.

      Pattern matching and null

      At the moment, both switch statements and switch expressions raise a NullPointerException if the selector expression evaluates to the null value. This was a reasonable semantics given the restricted types initially permitted for the selector expression. However, if switch allows a selector expression of any type, and we can write explicit type patterns in the switch labels, this design seems less appropriate.

      Our proposal is to gracefully extend the semantics of switch. First we introduce a new case label, written case null, that can be used in a switch to handle the case where the selector expression evaluates to the null value. For example:

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

      In this case we would expect the expression nullOrString(null) to complete normally, printing the string Null.

      The behavior of the switch body when the value of the selector expression is the null value will now be determined by the case labels. If there are no occurrences of the null case label, then the switch will continue to raise a NullPointerException, just as before. But, if there is a null case label, then this label matches the null value, and the code associated with it will be executed.

      Another way to view this behavior is that any existing switch without a null case label in the switch body, has a new clause equivalent to case null: throw new NullPointerException(); inserted implicitly. Then every switch has a label that can match the null value.

      This means that any existing switch over a String or enum type executes exactly as it did before. But now developers, should they wish, can handle any possible null value case themselves, directly in the switch:

      static void improvedStringSwitch(String s) {
          // No longer needed!
          //
          // if (s == null) {
          //    System.out.println("oops!");
          //    return;    
          // }
          switch (s) {
              case null         -> System.out.println("oops!");
              case "Foo", "Bar" -> System.out.println("My favourite!");
              default           -> System.out.println("Acceptable...");
          }
      }

      The desire to maintain backwards compatibility with the current semantics of switch, means that we can not allow the default label to match the null value.

      It is sometimes the case that we wish to handle the null value in the same way as a value of a particular type. To this end, we introduce another new case label -- a null and type pattern case label, written case null, T t. A case label null, T t matches a target value if it is either the null value or a non-null value that matches the type pattern T t. In both cases the pattern variable t is initialized to the target value.

      For example, in the following code, the case label case null, String s matches both the null value and String values:

      static void nullTest2(Object o) {
          switch (o) {
              case Integer i       -> System.out.println("Integer: " + i);
              case Byte b          -> System.out.println("Byte: " + b);
              case Long l          -> System.out.println("Long: " + l);
              case Double d        -> System.out.println("Double: " + d);
              case null, String s  -> System.out.println("String: " + s);
              default              -> System.out.println(o.toString());
          }
      }

      Finally, we sometimes want a default label to also match a null value. To that end, we introduce another new case label, a null and default case label, written case null, default. The case label null, default matches all values (of a reference type), including the null value.

      static void nullDefaultTest(Object o) {
          // No longer needed!
          //
          // if (o == null) {
          //    System.out.println("Not an integral type");
          //    return;    
          // }
          switch (o) {
              case Byte b        -> System.out.println("Byte: " + b);
              case Short s       -> System.out.println("Short: " + s);
              case Integer i     -> System.out.println("Integer: " + i);
              case Long l        -> System.out.println("Long: " + l);
              case Char c        -> System.out.println("Char: " + c);
              case null, default -> System.out.println("Not an integral type"); 
          }
      }

      Guard patterns and conditional-and pattern operator

      Further experimentation with pattern switch suggests some further common scenarios where there is a need to add new patterns. Consider the following where we wish to use a pattern switch statement over a Shape value:

      class Shape { } 
      class Triangle extends Shape { 
          int calculateArea() { ... } 
      } 
      class Rectangle extends Shape { }
      
      class Test { 
          static void testTriangle(Shape s) { 
              switch (s) { 
                  case null: 
                      break;
                  case Triangle t: 
                      if (t.calculateArea() > 100 ) { 
                          System.out.println("Large Triangle"); 
                          break; 
                      } 
                  default: 
                      System.out.println("A shape (including small triangles"); 
              } 
          } 
      } 

      The intent of this code is to have a special case for a triangle with a calculated area greater than 100, and a default case for everything else (including triangles with a small area). But we can't express this directly using a single pattern. We have to write a case label that matches all triangles, and place the test about the size of the triangle rather uncomfortably within the corresponding statement group. Furthermore, we then have to use fall-through to get the correct behavior when the triangle has an area less than or equal to 100.

      The issue here is that just supporting a switch using a single pattern to discriminate the cases doesn't scale beyond a single condition. We need some way to express a refinement to a pattern. One simple approach might be to extend the grammar of case labels to provide such a refinement; this is often called a guard in other languages. For example, we could introduce a new keyword where to appear at the end of a case label and be followed by a boolean expression; for example: case Triangle t where t.calculateArea() > 100.

      Rather than take this approach, we follow a different but more expressive strategy. We do not extend the language of case labels but add new patterns instead. We add (1) a new pattern called a guard pattern, written either true(b) or false(b) that allows an arbitrary boolean-valued expression b to be used as a pattern, and (2) a conditional-and operator, written &, that combines two patterns to form a single pattern. The testTriangle method can then be rewritten as follows:

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

      The case label uses a conditional-and pattern. A value matches the conditional-and pattern Triangle t & true(t.calculateArea() > 100) if, first, it first matches the type pattern Triangle t, and, if so, it also matches the pattern true(t.calculateArea() > 100). A value matches the guard pattern true(t.calculateArea() > 100) if the subexpression evaluates to true.

      We have used & to denote the conditional-and pattern operator, but this syntactic choice is tentative and subject to change.

      By adding new patterns, we can eliminate the use of fall-through and allows the switch statement to use safer switch rules, rather than statement groups.

      A switch can test both for a refined and non-refined pattern in the same switch block:

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

      Description

      The purpose of this JEP is to (1) extend both switch expressions and statements to support patterns in case labels instead of just constants; (2) add new patterns to support this extension.

      The grammar for a switch label in a switch block will become:

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

      Thus a case label has either (i) one or more case constants (a case constant is either a constant expression or the name of an enum constant), (ii) a null, (iii) a null and a type pattern, (iv) a null and default, or (v) a pattern.

      These new labels can be used in a switch block regardless of whether it consists of switch rules (those that use ->) or switch labeled statement groups (those that use :). For example, this switch block uses switch rules:

      System.out.println(switch(o) {
          case String s -> s;
          default -> "Not a string";
      })

      and this uses statement groups:

      switch(o) {
          case String s: 
              System.out.println(s);
              break;
          default:
              System.out.println("Not a string");
      }

      At a high-level, the semantics of switch is broadly unchanged: the value of the selector expression is compared to the switch labels, one is determined to match (including a possible default) and the code associated with the label is then executed. Until now the notion of matching was essentially equality with a case constant. For case labels involving patterns, equality is replaced by pattern matching. Returning to a variant of our earlier example:

      String formatted;
      Object obj = new Long(12L);
      switch (obj) {
          case Integer i: formatted = String.format("int %d", i); break;
          case Byte b:    formatted = String.format("byte %d", b); break;
          case Long l:    formatted = String.format("long %d", l); break;
          case Double d:  formatted = String.format("double %f", d); break;
          case String s:  formatted = String.format("String %s", s); break
          default:        formatted = obj.toString();
      }

      At run-time, pattern matching the value of obj with the pattern Long l succeeds, so this statement group will be executed. Recall that in the process of pattern matching, the pattern variable l is initialized with the Long value. This variable is in scope for switch labeled statement group associated with the case pattern. (In this case, this means only the two statements formatted = String.format("long %d", l); and the following break statement.)

      We also propose to add new patterns to support pattern matching with switch. A companion JEP proposes adding two new patterns to the type patterns of Java 16 SE: (1) record patterns, and (2) array patterns. Assuming these patterns have already been added, the grammar for patterns will become the following:

      Pattern:
      PatternOperand
      Pattern & PatternOperandOrGuard
      PatternOperandOrGuard:
      PatternOperand
      GuardPattern
      PatternOperand:
      TypePattern
      ArrayPattern
      RecordPattern
      TypePattern:
      LocalVariableDeclaration
      ArrayPattern:
      ArrayType ArrayComponentsPattern
      ArrayComponentsPattern:
      { [ ComponentPatternList [ , ... ] ] }
      ComponentPatternList:
      ComponentPattern { , ComponentPattern }
      ComponentPattern:
      Pattern
      ArrayComponentsPattern
      RecordPattern:
      ReferenceType ( [ ArgumentPatternList ] [ , ...] )
      ArgumentPatternList :
      ArgumentPattern { , ArgumentPattern }
      ArgumentPattern:
      Pattern
      GuardPattern:
      BooleanLiteral ( Expression )

      The grammar has been carefully designed to exclude a guard pattern as a valid top-level pattern. There is little point in writing pattern matching code such as o instanceof true(s.length != 0). Guard patterns are intended to be refine the meaning of other patterns. The grammar reflects this intuition.

      Pattern matching is a natural fit for switch. But there is a lot of detail. In the following subsections we deep-dive into those details, examining what needs to be added and what needs to be changed to support patterns in switch.

      New Patterns

      This JEP proposes two extensions to patterns:

      1. A new pattern, the guard pattern, and
      2. A conditional-and operator on patterns.

      A guard pattern is of the form true(e) or false(e), where the subexpression e is required to be a valid boolean expression. Guard patterns do not introduce any pattern variables.

      A value (including null) matches a guard pattern true(e) if the expression e evaluates to true. A value (including null) matches a guard pattern false(e) if the expression e evaluates to false.

      Any two patterns can be combined into a single pattern using the conditional-and pattern operator, written &, provided that the first pattern is not a guard pattern. A value matches a conditional-and pattern p & q if it first matches the pattern p, and then, secondly, if it matches the pattern q. If a value does not match the first pattern operand, then no attempt is made to match it against the second pattern operand.

      A conditional-and pattern p & q introduces the union of the pattern variables introduced by the patterns p and q. The scope of any pattern variable declaration in p includes the the pattern q. (This allows for a pattern such as String s & true(s.length() > 1). A value matches this pattern if it can be cast to a String and that string has a length of two or more characters.)

      There are five major design issues with patterns for switch considered in the rest of this description:

      1. Enhanced type checking of pattern switch
      2. New requirements for well-formed switch blocks
      3. Scopes of pattern variable declarations occurring in case labels
      4. Dealing with null, and
      5. Additional requirements when supporting switch expressions.

      Enhanced type checking of switch

      In the first iteration, we propose not to support a switch with case labels that are a mix of case constants and patterns (beyond allowing the new null labels). We introduce some terminology: a non-pattern switch is one where at least one switch label in its switch block is a case label with constants, and the rest are either the default label, a case label with constants, a case label with null, or a case label with null and default. A pattern switch is one where every switch label in its switch block is either the default label, a case label with null, a case label with null and default, a case label with null and a type pattern, or a case label with a pattern.

      For example, in the first iteration, the following example results in a compile-time error, as it is neither a non-pattern switch nor a pattern switch:

      static void error1(String s) {
          switch (s) {
              case "Hello world" -> 
                  System.out.println("Hello back");
              case String s ->                // Error! Can't mix constant and pattern labels
                  System.out.println("Nothing?");
          }
      }

      The type of the selector expression of a non-pattern switch will continue to be limited to be either an integral primitive type (char, byte, short, or int), their boxed form (Character, Byte, Short, or Integer), String, or an enum type.

      The type of the selector expression of a pattern switch can be either an integral primitive type, or any reference type.

      It is an open question whether the three remaining primitive types (boolean, float, and double) should be supported in a pattern switch. At the moment, we propose not to do so in the first iteration, as its utility seems minimal.

      This means that the following pattern switch is type-correct:

      static void test1(Object o) {
          switch(o) {             // Finally, a switch on an Object!
              case Integer i -> System.out.println("It's an Integer");
              case String s  -> System.out.println("It's a String");
          }
      }

      The switch block of a switch is compatible with the selector expression if the selector expression is compatible with all the switch labels appearing in the switch block. Four new rules are needed:

      1. A selector expression is compatible with the case label case null if the type of the selector expression is a reference type.

      2. A selector expression is compatible with the case label case null, T t if the selector expression is compatible with the type pattern T t (as defined in JLS 14.30.1).

      3. A selector expression is compatible with the case label case null, default if the type of the selector expression is a reference type.

      4. A selector expression is compatible with a case label with a pattern if it is compatible with the pattern.

      This allows for powerful yet safe type-based selection. In the following example, in a single switch expression, the selector expression is pattern matched with type patterns involving a class type, an enum type, a record type, and an array type:

      record Point(int i, int j){ }
      enum Color { RED, GREEN, BLUE; }
      
      class TypeTest {
          static void typeTester(Object o) {
              switch (o) {
                  case null -> 
                      System.out.println("null");
                  case String s -> 
                      System.out.println("String");
                  case int[] ia -> 
                      System.out.println("Array of ints of length" + ia.length);
                  case Point p ->
                      System.out.println("Record class: "+ p.toString());
                  case Color c ->
                      System.out.println("Enum Color with "+ c.values().length+" constants");
                  default ->
                      System.out.println("Something else");
              }
          }
      }

      Well-formed switch blocks

      In addition to type compatibility of the switch block with the selector expression, extra well-formedness conditions are required of the switch block.

      In a non-pattern switch, it is already an error if any two of the case constants associated with the switch block have the same value. For a pattern switch, things are a little more subtle.

      First, it should not be possible to have more than one match-all switch labels in a switch block. Two switch labels are said to be match-all: (i) the default label, and (ii) the null and default case label.

      Second, it should not be possible to have more than one null-matching case labels in a switch block. Three case labels are said to be null-matching: (i) case null, (ii) case null, T t, and (iii) case null, default.

      Thus the following example is erroneous:

      static void error2(Object o) {
          switch (o) {
              case null, String s -> 
                  System.out.println("String or null");
              case null, Integer i ->             // Error - two labels are null-matching
                  System.out.println("Integer or null");
          }
      }

      Lastly, it should be an error if a case label with a pattern is dominated by an earlier case label with a pattern. Consider the following problematic example:

      static void error2(Object o) {
          switch(o) {
              case CharSequence cs ->     
                  System.out.println("A Character Sequence of length " + cs.length()); 
              case String s ->            // Error - this pattern is dominated!
                  System.out.println("A String: " + s);
      
          }
      }

      Here the case label case CharSequence cs dominates the second case String s, because every value that matches the pattern String s also matches the pattern CharSequence cs (but not vice versa). This is because the type of the second pattern, String is a subtype of the type of the first pattern, CharSequence. (For the purposes of dominance, a case label case null, T t is treated as if it were a case label with the type pattern T t.)

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

      Scopes of pattern variable declarations occurring in case labels

      Pattern variables are local variables that are declared by patterns. Pattern variable declarations are unusual in that their scope is flow-based. As a recap, consider the following example:

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

      The pattern String s declares the pattern variable s. This declaration is in scope in the right hand operand of the && expression, as well as the "then" block. However, it is not in scope in the "else" block -- for control to transfer to the "else" block, the pattern matching must have failed, in which case the pattern variable will not have been initialized.

      This flow-sensitive notion of scope for pattern variable declarations needs to be extended to those pattern declarations occurring in case labels. Two new rules about the scope of pattern variable declarations need to be added.

      1. The scope of a pattern variable declaration occurring in a case label of a switch rule, includes the expression, block, or throw statement that appears to the right of the ->.

      2. The scope of a pattern variable declaration occurring in a case label of a switch labeled statement group, where there are no further switch labels that follow, includes the block statements of the statement group.

      The following example shows the first rule in action:

      static void test3(Object o) throws MyAppException {
          switch(o) {
              case String s & true(s.length==1) ->
                      System.out.println(s);
              case Character c -> {
                  if (c.charValue() == 7) {
                      System.out.println("Ding!");
                  }
                  System.out.println("Character");
              }
              case Integer i ->
                      throw new MyAppException(i);
          }
      }

      The scope of the declaration of the pattern variable s in the pattern label of the first switch rule includes the guard pattern and the statement expression to the right of the ->. The scope of the declaration of the pattern variable c in the pattern label of the second switch rule is the block to the right of the ->. Finally, the scope of the declaration of the pattern variable i in the pattern label of the third switch rule is the throw statement to the right of the ->.

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

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

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

      The possibility of falling through a switch label that declares a pattern variable must be excluded as a compile-time error.

      Consider the following erroneous example:

      static void test5(Object o) {
          switch(o) {
              case Character c:
                  if (c.charValue() == 7) {
                      System.out.print("Ding ");
                  }
                  if (c.charValue() == 9) {
                      System.out.print("Tab ");
                  }
                  System.out.print("character");
              case Integer i:                 // Compile-time error
                  System.out.println("An integer " + i);
          }
      }

      If this were allowed and the value of the o parameter was a Character then execution of the switch block could fall through the second switch label and the pattern variable i would not have been initialized. The problem is allowing the execution to fall through a label that declares a pattern variable. This must be considered a compile-time error.

      On the other hand, falling through a label that does not declare a pattern variable is safe.

      void test6(Object o) {
          switch(o) {
              case String s:
                  StringCounter++;
              default : 
                  System.out.println("Done");
          }
      }

      null and pattern switch

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

      However, given that there is an existing reasonable (non-exceptional) semantics for pattern matching and null values, there is an opportunity to extend switch to be more null-friendly, and yet remain compatible with existing switch semantics.

      We introduce three new null-matching case labels: case null, case null, T t and case null, default. The intent is that the first matches when the value of the selector expression is null. The second matches when the value of the selector is either null or a non-null value that can be cast to the type T without raising a ClassCastException. The third matches when either the value of the selector is null or no other case labels match.

      We lift the blanket restriction that a switch always raises a NullPointerException if the value of the selector expression is the null value. Instead the case labels are inspected to determine the behavior of the switch. If the selector expression evaluates to null, any of the three null-matching labels are said to match. In the case that there is no null-matching label associated with the switch block, the switch raises a NullPointerException, just as before.

      If the selector expression has a non-null value, then we use pattern matching to compare the value against any case labels with patterns. (As usual, if no case label matches, then any match-all switch label is considered to match.)

      For example:

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

      The expression test7(null) will print null! rather than raise a NullPointerException.

      This new behavior around null is as if the compiler automatically inserts a new clause whose case label is case null and whose body raises a NullPointerException. In other words, the following code:

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

      is equivalent to

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

      Evaluating the expression test8a(null) (and test8b(null)) raises a NullPointerException, as expected.

      We keep the intuition from the existing switch construct that performing a switch over a null is an exceptional thing to do; the difference is that you are provided with a mechanism to directly handle this case internally within the switch and not externally via exception handling or null-checking. If you choose not to have a null-matching case label in your switch body, then switching over a null value will raise a NullPointerException as before.

      switch Expressions

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

      Consider the following (erroneous) switch expression:

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

      The switch block has only one switch label, case String s. This matches any value of the selector expression whose type is a subtype of String. In other words, we say that the type coverage of this rule is every subtype of String. The reason that this switch is incomplete is that the type coverage of its switch block does not include the type of the selector expression.

      Consider the following (still erroneous) example:

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

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

      The coverage of the switch label default is clearly every type, so the following example is permitted:

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

      Should the type of the selector expression be a sealed class, then the type coverage check can take into account the permits clause of the class to determine whether a switch block is complete. Consider the following example:

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

      We have a sealed interface, S, with three final classes, A, B and C (recall that all record classes are implicitly final).

      In this case, the compiler can determine that the type coverage of the switch block is the types A, B, and C, but as the type of the selector expression, S, is a sealed interface whose permitted subclasses are exactly A, B, and C, this switch block is complete (and, in particular, no default case is needed).

      (In this case, the compiler will automatically actually add a default case that throws a IncompatibleClassChangeError exception. This case can only be reached if the sealed interface has been changed and the switch code has not been recompiled. The compiler hardens your code automatically for you.)

      The reader might have noticed that this requirement of a pattern switch expression to be complete is analogous to the treatment of a switch expression over an enum class, where a default case is not required if there is a clause for every enum constant of the enum class. For example:

      enum Color { RED, GREEN, BLUE }
        class EnumCoverage {
            static int testColor(Color c) {
                return switch(c) {
                    case RED   -> 1;
                    case GREEN -> 2;
                    case BLUE  -> 3;
                    // no default needed
                };
            }
        }

      It is also the case that should the enum class have been changed by, for example, adding a new enum constant, but the class EnumCoverage has not been recompiled, then evaluating the switch expression completes abruptly with an IncompatibleClassChangeError exception.

      Future Work

      Adding patterns in switch expressions and statements is just a step in a comprehensive program of enriching Java with pattern matching. Its utility will be significantly enhanced with the pattern forms supporting nesting that are proposed in a companion JEP.

      Possible areas for future work (either in future iterations of this JEP or in other JEPs) include:

      Deconstruction Patterns. Many classes are simply carriers for their data. We construct them with constructors, which take a vector of N arguments and produce an aggregate, but we generally fetch the components one at a time, with accessors. Just as we can combine the type-test/cast/bind operations into a single type-test pattern, we can combine the type-test/cast/extract-multiple into a single deconstruction pattern. If we have a hierarchy of Node with subtypes for IntNode (containing a single int), AddNode and MulNode (containing two nodes), and NegNode (containing a single node), we can match against a Node and act on the specific subtypes all in one step:

      int eval(Node n) {
          switch(n) {
              case IntNode(int i): return i;
              case NegNode(Node n): return -eval(n);
              case AddNode(Node left, Node right): return eval(left) + eval(right);
              case MulNode(Node left, Node right): return eval(left) * eval(right);
              default: throw new IllegalStateException(n);
          };
      }

      Today, to express ad-hoc polymorphic calculations like this, we would use the "Visitor" pattern. Using pattern matching is generally more transparent and straightforward.

      Alternatives

      Rather than supporting pattern matching in switch, a "typeswitch" could be provided instead. This feature is simpler to specify and implement but considerably less expressive.

      An alternative to adding guard patterns and the conditional-and operator for patterns, is to support guards in switch, which would be a special syntactic extension for switch labels with patterns, e.g.

      SwitchLabel:
      case Pattern [ when Expression ]
      ...

      Guard patterns and the conditional-and operator offer considerably greater expressivity and flexibility, and do not require the introduction of another contextual keyword.

      Dependencies

      This JEP builds on pattern matching in instanceof operator (JEP 394) and also the enhancements offered by switch expressions (JEP 361). It is hoped that this JEP will coincide with a companion JEP that proposes two new patterns that support nesting. The implementation will likely make use of Dynamic Constants in the JVM.

        Attachments

          Activity

            People

            • Assignee:
              gbierman Gavin Bierman
              Reporter:
              gbierman Gavin Bierman
              Owner:
              Gavin Bierman
              Reviewed By:
              Brian Goetz
            • Votes:
              0 Vote for this issue
              Watchers:
              10 Start watching this issue

              Dates

              • Created:
                Updated: