Monday, September 10, 2018

JDK 12: Switch Statements/Expressions in Action

My last post "Playing with JDK 12's Switch Expressions" talked about use of the JDK 12 Early Access Builds to try out JEP 325 switch expressions and statements and provided a simple example. This post uses the same JDK 12 Early Access Build 10 to demonstrate different features of switch expressions and enhanced switch statements.

I used a 2x2 grid in the blog post "Enhancing Java switch Statement with Introduction of switch Expression" to illustrate that the new "arrow" syntax ("switch labeled rule") can be used with a switch statement or with a switch expression per JEP 325. Similarly, per JEP 325, the traditional "colon" syntax ("switch labeled statement group") can also be used with either a switch expression or with a switch statement. In other words, presence of the colon (:) does NOT necessarily imply a switch statement and presence of an "arrow" (->) does NOT necessarily imply a switch expression. For convenience, I've included an adapted version of the table shown in my earlier post here.

  STATEMENT
("Nonlocal control flow _out_ of a switch [continue to an enclosing loop, break with label, return]")
EXPRESSION
(Totality: return a value)
SWITCH LABELED STATEMENT GROUP
("Colon")
(Enables Fall-through)
switch we know and "love", but enhanced break returns a value like return
SWITCH LABELED RULE
("Arrow")
(Prevents Fall-through)
"Syntactic shorthand" for Statement/Colon (above) plus
  • "obviates the annoyance of 'break'"
  • "implicitly prevents fallthrough of all forms"
  • "avoids the confusion of current switch scoping"
Arrow (->) points to returned value

With the JDK 12 Early Access Builds, it is convenient to try the new switch expression out and we can also try out the traditional and enhanced versions of the switch statement.

Traditional switch Statement

The traditional switch statement that we "know and love" is still available even with JDK 12 preview enabled (--enable-preview). An example of this traditional switch statement that compiles and executes successfully even with JDK 12 language feature preview enabled is shown next.

/**
 * Demonstrate traditional switch statement assigned to
 * local variable.
 */
public static void demonstrateTraditionalSwitchStatement()
{
   out.println("Traditional Switch Statement:");
   final int integer = 3;
   String numericString;
   switch (integer)
   {
      case 1 :
         numericString = "one";
         break;
      case 2 :
         numericString = "two";
         break;
      case 3:
         numericString = "three";
         break;
      default:
         numericString = "N/A";
   }
   out.println("\t" + integer + " ==> " + numericString);
}

This and all other code examples shown in this post are available on GitHub. This particular example shows a common use of the traditional switch statement to set a local variable's value. I intentionally chose this use case because a new switch expression is an improved approach to accomplishing this.

Enhanced switch Statement

As stated previously, we can use the new "arrow" syntax ("switch labeled rules") with the enhanced switch statement. This is shown in the next code example that compiles and runs against the JDK 12 Early Access Build 10 when --enalved-preview is used.

/**
 * Demonstrate enhanced switch statement used to assign
 * a local variable.
 */
public static void demonstrateEnhancedSwitchStatement()
{
   out.println("Enhanced Switch Statement:");
   final int integer = 2;
   String numericString;
   switch (integer)
   {
      case 1 -> numericString = "one";
      case 2 -> numericString = "two";
      case 3 -> numericString = "three";
      default -> numericString = "N/A";
   }
   out.println("\t" + integer + " ==> " + numericString);
}

This last examples shows the switch still being used as a statement, but in this case it takes advantage of the "arrow" syntax ("label rules") to accomplish its switching without explicit specification of break. This is not only less code, but more importantly has the advantage of not allowing for the often dreaded switch "fall-through." In short, the enhanced switch statement works like the current/traditional switch statement, but without the potential warts of the traditional version.

New switch Expression Returning Value via break

Beyond enhancing the current switch statement to allow for specification of a switch statement without risk of fall-through, JEP 325 also introduces the concept of using the switch keyword in a switch expression. The Java Tutorial's "Expressions, Statements, and Blocks" page explains the differences between statements and operations. For purposes of this discussion, two of the important observations made in that tutorial are (I added bold emphasis):

  1. "An expression is a construct made up of variables, operators, and method invocations ... that evaluates to a single value."
  2. "The Java programming language allows you to construct compound expressions from various smaller expressions as long as the data type required by one part of the expression matches the data type of the other."

The next code listing demonstrates how, with JDK 12 Early Access Build 10 and --enable-preview, one can replace the code shown above that used a switch statement to assign a value to an earlier declared local variable with a single statement that uses a switch expression to assign its result value to the local variable in a single statement.

/**
 * Demonstrate switch expression using colons and breaks.
 */
public static void demonstrateSwitchExpressionWithBreaks()
{
   final int integer = 1;
   out.println("Switch Expression with Colons/Breaks:");
   final String numericString =
      switch (integer)
      {
         case 1 :
            break "uno";
         case 2 :
            break "dos";
         case 3 :
            break "tres";
         default :
            break "N/A";
      };
   out.println("\t" + integer + " ==> " + numericString);
}

The code example just shown demonstrates use of a switch expression that looks very similar to the traditional switch statement example shown earlier. However, there are a couple significant differences. One difference is that this switch expression returns a result that is assigned to the local variable "numericString". The second difference, which directly relates to the switch expression being able to return a value, is that the break clauses now each have the value to be returned for the relevant case immediately specified after the break keyword. In essence, the break in the switch expression acts like a Java method return.

New switch Expression Returning Value via Label Rules

The example just shown demonstrates that one can return a value from a switch expression with similar colon (:) and break syntax to what one is likely used with switch statements. Besides being familiar, the other advantage of this is that one can specify multiple statements to occur for a single case before returning a single value. In most cases, however, it will likely become popular to return a value from a switch expression using the "arrow" syntax discussed earlier to benefit from no risk of fall-through and to avoid scope surprises commonly associated with the traditional switch statement. The next code listing demonstrates how the new switch expression can use "label rules" ("arrow" syntax) instead of colon and break to elegantly return a single resolved value for the switch.

/**
 * Demonstrate switch expressions using "arrow" syntax.
 */
public static void demonstrateSwitchExpressionWithArrows()
{
   final int integer = 4;
   out.println("Switch Expression with Arrows:");
   final String numericString =
      switch (integer)
      {
         case 1 -> "uno";
         case 2 -> "dos";
         case 3 -> "tres";
         case 4 -> "quatro";
         default -> "N/A";
      };
   out.println("\t" + integer + " ==> " + numericString);
}

The four examples above demonstrate each of the cases shown in the 2x2 grid. The remainder of this post will discuss some additional observations from trying out switch expressions and statements with JDK 12 Early Access Build 10.

Multiple Constants Can Be Specified for a Single case

Any of the four quadrants in the 2x2 grid allow for multiple constants to be associated with a single case. This is demonstrated in the next code listing that compiles and runs with JDK 12 Early Access Build 10 with "preview language features" enabled.

/**
 * Demonstrate that multiple constants can be associated with
 * a single {@code case} and used in conjunction with a
 * {@code switch} expression that uses the "arrow" syntax.
 */
public static void demonstrateLabelRulesWithSharedCases()
{
   final int integer = 7;
   out.println("Multiple Case Labels:");
   final String numericString =
      switch (integer)
      {
         case 0 -> "zero";
         case 1, 3, 5, 7, 9 -> "odd";
         case 2, 4, 6, 8, 10 -> "even";
         default -> "N/A";
      };
   out.println("\t" + integer + " ==> " + numericString);
}

/**
 * Demonstrate that multiple constants can be associated with
 * a single {@code case} and used in conjunction with a
 * {@code switch} statement that uses the traditional colon and
 * {@code break} syntax.
 */
public static void demonstrateBlockedStatementsWithSharedCases()
{
   final int integer = 6;
   out.println("Multiple Case Labels:");
   String numericString;
   switch (integer)
   {
      case 0:
         numericString = "zero";
         break;
      case 1, 3, 5, 7, 9:
         numericString = "odd";
         break;
      case 2, 4, 6, 8, 10:
         numericString = "even";
         break;
      default:
         numericString = "N/A";
   };
   out.println("\t" + integer + " ==> " + numericString);
}

"Arrow" ("label rules") and Colon/break ("statement group") Cannot Be Mixed

The JDK 12 Early Access Build 10 compiler (javac) does NOT allow the mixing of the "arrow" syntax and the traditional colon/break syntax. Attempting to mix these results in the error message: "error: different kinds used in the switch". An example of code that would not compile and would show this particular error message is shown next.

/**
 * WARNING - This does NOT compile, even with JDK 12 Early
 * Access Builds and --enable-preview because JEP 325 does
 * not allow the "arrow" syntax to be mixed with the
 * traditional colon/break syntax.
 */
public static void demonstrateMixed()
{
   final int integer = 3;
   String numericString;
   switch(integer)
   {
      case 1 :
         numericString = "one";
         break;
      case 2 -> numericString = "two";
      default -> numericString = "N/A";
   }
   return numericString;
}

switch Statement's break Cannot Return Value

The new switch expression returns a value and when the colon and break approach are used by the switch expression, that returned value is designated immediately following the break keyword. Because the traditional switch statement does not return a value, it is a compile-time error to attempt to have a break associated with a switch statement designate a return value. The error ("error: unexpected value break") can be reproduced with the following code.

/**
 * WARNING - This does NOT compile, even with JDK 12 Early
 * Access Builds and --enable-preview because it is
 * nonsensical to have a "statement" return a value; that
 * is what an expression should be used for.
 */
public static void demonstrateSwitchStatementReturnedLabel()
{
   final int integer = 4;
   switch (integer)
   {
      case 1:
         break "one";
      case 2:
         break "two";
      case 3:
         break "three";
      default:
         break "N/A";
   };
}

When one attempts to compile the above code using JDK 12 Early Access Build 10's javac compiler with flags --enable-preview and -release 12 specified, four instances (corresponding to the three case plus one default) of the error message "error: unexpected value break" are seen. Not surprisingly, the simple change of assigning this switch to a local variable (and effectively turning the statement into an expression) allows this code to compile. In other words, changing the code above to the code in the next code listing allows it to compile and run successfully.

/**
 * This demonstrates that a {@code switch} "expression" is
 * able to (and expected to) provide the "return" value for
 * a given {@code case} and {@code default} instead of being
 * a compiler error as it was for the "statement" example
 * demonstrated in method
 * {@link #demonstrateSwitchStatementReturnedLabel()}.
 */
public static void demonstrateSwitchExpressReturnedLabel()
{
   final int integer = 4;
   final String numericString =
   switch (integer)
   {
      case 1:
         break "one";
      case 2:
         break "two";
      case 3:
         break "three";
      default:
         break "N/A";
   };
}

The current JEP 325 text includes a discussion on how this break behavior is similar to methods' return. That discussion points out that the switch statement requiring no returned value after its breaks is analogous to a method returning void. A switch expression is expected to return a non-void value.

switch Statement's "Arrow" Syntax Must Point to a Statement

The following code will not compile with JDK 12 Early Access Build 10 even with --enable-preview and -release 12 provided to the javac compiler.

/**
 * WARNING - This does not compile, even with JDK 12 Early
 * Access Builds and --enable-preview and reports error message
 * "error: not a statement" because it is expecting a
 * {@code switch} "statement" but what is being provided to each
 * {@code case} is NOT a statement.
 */
public static void demonstrateSwitchStatementReturnedValueViaLabelRule()
{
   final int integer = 5;
   switch (integer)
   {
      case 1 -> "one";
      case 2 -> "two";
   };
   out.println(numericString);
}

The above code does not compile and the error message reported is, "error: not a statement". This is because the switch is being used as a statement in this example, but the "arrow" syntax is "pointing" to literal strings rather than to a valid Java statement.

All Possibilities Must be Specified in a switch Expression

Because a switch expression needs to return a non-void value, a switch expression must specify a case for all possible values it might switch on. In practice, this is likely to be accomplished via a default to catch all possibilities not explicitly specified with case. With a traditional switch statement, it was not required to ensure that all possible values being switched on were covered by a case or default and that led sometimes to conditions such as I described in the blog post "Log Unexpected Switch Options".

The following code violates the rule that a switch expression must specify all possible values in either a case or via default:

/**
 * WARNING - This method will not compile even with JDK 12
 * Early Access Build 10 with --enable-preview because of
 * error; "the switch expression does not cover all possible
 * input values".
 */
public static void demonstrateLackingCaseInSwitchExpression()
{
   final int integer = 5;
   String numericString =
      switch (integer)
      {
         case 1 -> "one";
         case 2 -> "two";
      };
   out.println(numericString);
}

The code just shown will not compile and the causal error message is, "error: the switch expression does not cover all possible input values."

The Effect of JEP 325 on Future Use of switch

Considering the possibilities presented by the availability of switch expressions in Java being available in addition to switch statements, and considering the advantages offered by the new "arrow" syntax that can be used with switch expressions or statements, it is interesting to begin thinking about when each quadrant in the above 2x2 grid is most beneficial. In general, I believe I will find myself using the switch expression with "arrow" syntax ("label rules") most often with enhanced switch statements using "arrow" syntax also being frequently used. I suspect I'll use the traditional : (break) syntax far less often in the future. Even when I have multiple statements to be executed for a particular case, I'll likely factor those statements into a single method that can be called in the case using the "arrow" syntax. This will allow me to benefit from more obvious scoping and avoid the risks of fall-through. Given the ability to specify multiple constants for a single case that will now be available, fall-through won't be necessary anymore even in cases where multiple cases lead to the same result.

Additional Resources

5 comments:

@DustinMarx said...

The article "The Complete Guide to the Java SE 12 Extended Switch Statement/Expression" was recently published.

@DustinMarx said...

There is a call for feedback related to the enhanced switch (a preview feature that may become a "permanent feature" in JDK 13). The "only [anticipated] change" is to "change 'break value' to 'break-with value'."

There is also interesting discussion regarding early IntelliJ IDEA support for the switch expression preview feature.

@DustinMarx said...

The post "Draft JEP: Switch Expressions" on the amber-dev mailing list announces that the "draft JEP to finalize Switch Expressions is here" and provides a link to the draft JEP. The most significant change from the preview feature version of this in JDK 12 is that the keyword for returning a value from a switch expression is now the hyphenated keyword break-with instead of break.

@DustinMarx said...

It has been proposed that JEP 354 ["Switch Expressions (Preview)"] be targeted to JDK 13.

@DustinMarx said...

Mark Reinhold's post "New candidate JEP: 361: Switch Expressions (Standard)" announces candidate JEP 361: Switch Expressions (Standard). Gavin Bierman has responded, reminding readers that "we are considering making this a permanent feature, so this is your last chance to provide substantive feedback based on any new experience you may have had with this feature." Bierman also provides a link to the latest JEP 361 draft language specification.