Tuesday, September 11, 2018

JDK 12 Switch Expression Encountering Unanticipated Enum Value

As I wrote about in my post "Playing with JDK 12's Switch Expressions", the JDK 12 Early Access Builds have made it easy to experiment with the implementation of JEP 325 ["Switch Expressions (Preview)"]. My post "JDK 12: Switch Statements/Expressions in Action" used code examples to demonstrate core features and characteristics of the enhanced switch statement and the new switch expression. In this post, I look at a special case explicitly called out in JEP 325 related to a runtime addition to an enum used in a switch expression.

Because a switch expression returns a value, it is necessary that all possible cases the switch might encounter are handled via a case (or covered by a default for those not explicitly associated with a case). JEP 325 states the following:

The cases of a switch expression must be exhaustive; for any possible value there must be a matching switch label. In practice this normally means simply that a default clause is required; however, in the case of an enum switch expression that covers all known cases (and eventually, switch expressions over sealed types), a default clause can be inserted by the compiler that indicates that the enum definition has changed between compile-time and runtime. (This is what developers do by hand today, but having the compiler insert it is both less intrusive and likely to have a more descriptive error message than the ones written by hand.)

I have written code similar to that described in JEP 325 ("this is what developers do by hand today") as discussed in my blog post "Log Unexpected Switch Options." In the past, it was often wise to add logic for handling or logging switch statement options that were not explicitly called out or handled in a default. With the advent of switch expressions via JDK 12 and JEP 325, it is now required.

JEP 325 addresses the case of a switch expression on an enum and explicitly specifies how to support situations when all of an enum's values were explicitly specified in case clauses when the enum and code with the switch expression were compiled, but then more values were later added to the enum without recompiling the switch expression code using that enum.

To demonstrate this support, I will present a simple enum along with two examples based on JEP 325 and the JDK Early Access Build 10 to use that enum in a switch statement and a switch expression.

The following code listing shows a simple enum called Response that only has two values.

package dustin.examples.jdk12.switchexp;

/**
 * Enum representation of a response.
 */
public enum Response
{
   YES,
   NO;
}

The next code listing shows a class that includes two methods that use the enum shown above. One method uses a switch statement against that enum and the other uses a switch expression against that enum.

package dustin.examples.jdk12.switchexp;

import static java.lang.System.out;

/**
 * Demonstrates implicit handling of expanding enum
 * definition related to JEP 325 switch expressions and
 * switch statements.
 */
public class GrowingEnumSwitchDemo
{
   public static void printResponseStringFromStatement(final Response response)
   {
      out.println("Statement [" + response.name() + "]:");
      switch (response)
      {
         case YES:
            out.println("Si!");
            break;
         case NO:
            out.println("No!");
            break;
      }
   }

   public static void printResponseStringFromExpression(final Response response)
   {
      out.println("Expression [" + response.name() + "]:");
      out.println(
         switch (response)
         {
            case YES -> "Si!";
            case NO -> "No!";
         });
   }

   public static void main(final String[] arguments)
   {
      if (arguments.length < 1)
      {
         out.println("Provide an appropriate 'dustin.examples.jdk12.switchexp.Response' string as an argument.");
         System.exit(-1);
      }
      final String responseString = arguments[0];
      out.println("Processing string '" + responseString + "'.");
      final Response response = Response.valueOf(responseString);
      printResponseStringFromStatement(response);
      printResponseStringFromExpression(response);
   }
}

The code above (which is also available on GitHub) will compile without incident and when I execute the main function on the GrowingEnumSwitchDemo class and pass it the "YES" string, it works as expected. If I add a new value MAYBE to the Response enum and compile only that enum Java file and then run the GrowingEnumSwitchDemo.main(String[]) with string "MAYBE", I encounter an IncompatibleClassChangeError. The new Response.java listing is shown next, followed by a screen snapshot that demonstrates the issue just described once the enum only was re-compiled with new value and run with the previously compiled calling code.

package dustin.examples.jdk12.switchexp;

/**
 * Enum representation of a response.
 */
public enum Response
{
   YES,
   NO,
   MAYBE;
}

The presence of the IncompatibleClassChangeError makes it obvious immediately that there is a new value on the enum not previously handed by the switch expression. This allows the developer to fix the switch expression either by adding a case for the enum value or by adding a catch-all default. This is likely to be better than the current situation today where a switch statement using the :/break syntax will silently move on without exception message in the same situation (which is also demonstrated in the previous code listing and screen snapshot).

There are several things to like about the enhancements coming to Java via JEP 325. The "arrow" syntax allows switch expressions and switch statements to not be burdened with surprising scope issues, risk of unintentional fall-through, or need for explicit breaks. Furthermore, switch expressions, which must return a value, can be used in conjunction with enums to ensure that all enum values are always handled at compile-time (won't compile if all enum values are not handled at compile-time) or that an error is thrown if the enum being used has a value added to it and is used with the previously compiled client code.

No comments: