According to some surveys such as that of JetBrains, version 8 of Java is currently the most used by developers all over the world, despite being a 2014 release.
What you are reading is an article in a series titled “Going beyond Java 8”, inspired by the contents of my book “Java for Aliens”. These articles will guide the reader step by step to explore the most important features introduced starting from version 9. The aim is to make the reader aware of how important it is to move forward from Java 8, explaining the enormous advantages that the latest versions of the language offer.
In this article we will see how Oracle with Java 14 has officially introduced an evolution of the switch
construct, which can now also be used as an expression, that is, it can return a value. This novelty is known as the switch
expression. However, the introduction of switch
expressions also caused the modification of the original construct, with the definition of a new word in the Java vocabulary (yield
), and the possibility of using the arrow notation in this construct, which was introduced with the lambda expressions with Java 8. We will therefore distinguish the switch
construct when used as a statement and as an expression, but rather than talking only about switch
expressions, it seems more appropriate to talk about a renewed construct: the new switch
, which can now be used as a statement or as expression.
To get a clearer picture of the situation, we also briefly describe the history of the construct (if you are not interested you can also skip the next section).
Why switch
has been updated
The switch
construct, like all Java constructs and the ternary operator, has been inherited from the C language since the first version of Java (an exception is the enhanced for loop that was introduced with Java 5).
This construct has characteristics that are well suited to the creation of typical programs that were once developed with this language, for example parsers and binary coders. Within Java programming, however, it has always been seen by programmers as a secondary construct. With the advent of enumerations in Java, switch
has taken on greater significance, but not enough to be considered a developer favorite. The syntax remained verbose, complex, and very different from the other constructs. The fall through technique is always complex to manage and error prone. The scope of construct-level local variables continues to be awkward. For these and other reasons, after more than 20 years, the need was felt to update the construct with new features. In fact, Oracle’s choice to speed up the Java development process through the six-monthly release of new versions, is well suited to the language update process, based on feedback and requests from developers. The platform is rapidly enriching with very interesting new features, after a few years in which its development had suffered an evident slowdown, due in part to the handover, and consequent need to take new directions between Sun Microsystems and Oracle. The new development cycle of Java based on the proposals of the developers, is giving life to a language that is more and more modern. In particular, with version 12, a new way of introducing a new feature in the language was also inaugurated: the feature preview. In practice, a new feature (the switch
expression) has become usable as preview in JDK 12. In order to take advantage of the new construct, it was necessary to specify particular options during compilation and at runtime. In this way, the developers had the opportunity to test the switch expressions, and to return fundamental feedback to Oracle. In fact, in version 13 of Java the implementation of the switch
expressions has been revived with some changes. In particular, the word yield
was introduced, thanks to the feedback from developers. In version 14 therefore, the switch
expressions were confirmed without further changes and were officially declared as new features of the language. So today, you no longer need to specify options to enable feature previews to use switch
expressions, they are effectively a new construct of Java programming.
The new switch
and the arrow notation
Within the switch
construct, it is now possible to use the arrow notation, which is the symbol consisting of the characters -> that we already use in lambda expressions. It may follow the case
keyword in place of the “:
” symbol. Suppose we have the following enumeration that defines the colors for a semaphore:
public enum Color { GREEN, YELLOW, RED; }
and then let’s consider the following class which represents a semaphore:
public class TrafficLight { public String changeColor(Color lightColor) { switch(lightColor) { case GREEN; turnOnGreenLight(); break; case YELLOW -> turnOnYellowLight(); break; case RED -> turnOnRedLight(); break; } } // rest of the code omitted }
Now we can rewrite the previous class as follows:
public class TrafficLight { public void changeColor(Color lightColor) { switch(lightColor) { case GREEN-> turnOnGreenLight(); case YELLOW -> turnOnYellowLight(); case RED -> turnOnRedLight(); } } // rest of the code omitted }
In the changeColor
method we used a switch
construct with arrow notation, which executes a statement for each case
. Note that there was no need to use the break
keyword. So, the syntax based on arrow notation makes our code less verbose, clearer and more modern.
Note that to specify multiple statements after arrow notation, you must include them in a pair of curly brackets. For example:
switch(lightColor) { case GREEN -> { turnOffAllLights(); turnOnGreenLight(); } // rest of the code omitted
The old switch
and the fall through technique
Consider the following snippet that uses the old syntax of the switch
construct, the Month
enumeration of the java.time
package, and the fall through technique:
Month month = getMonth(); String season; switch (month) { case DECEMBER: case JANUARY: case FEBRUARY: season = "winter"; break; case MARCH: case APRIL: case MAY: season = "spring"; break; case JUNE: case JULY: case AUGUST: season = "summer"; break; case SEPTEMBER: case OCTOBER: case NOVEMBER: season = "autumn"; break; default: season = "not identifiable"; break; }
The construct is verbose, not very elegant and not very readable, furthermore the syntax of the fall through technique easily leads to making mistakes. In fact, it is based on the omission of the break
statement that causes the execution of the following case
clauses, simulating what we can do with an if
construct based on multiple boolean expressions bound by OR
operators. For example, we could rewrite everything with an if
construct like the following:
Month month = getMonth(); String season; if (month == DECEMBER || month == JANUARY || month == FEBRUARY) { season = "winter"; } else if (month == MARCH || month == APRIL || month == MAY) { season = "spring"; } else if (month == JUNE || month == JULY || month == AUGUST) { season = "summer"; } else if (month == SEPTEMBER || month == OCTOBER || month == NOVEMBER) { season = "autumn"; } else { season = "not identifiable"; } }
The new switch
and the fall through technique
Now it is possible to use the arrow notation which can follow the keyword case
instead of the symbol “:
“. Let’s rewrite the previous example:
Month month = getMonth(); String season = null; switch (month) { case DECEMBER, JANUARY, FEBRUARY -> season = "winter"; case MARCH, APRIL, MAY -> season = "spring"; case JUNE, JULY, AUGUST -> season = "summer"; case SEPTEMBER, OCTOBER, NOVEMBER -> season = "autumn"; }
First of all, note that the various case
clauses can declare multiple labels separated by commas. In this way, there is no need to use fall through to perform the same instructions for different case
s, in particular, this syntax prevents us from using fall through. Obviously, there was no need to use the break
keyword. And the syntax is undoubtedly more concise and elegant, even compared to the version that uses the if
construct.
switch
expression
The switch
expression evolves the switch
construct that actually suffers from several defects. For example, forgetting a break
means causing an involuntary fall through. Furthermore, the applicability scenarios are limited compared to a classic if
, and the syntax is rather verbose. For these and other reasons, the switch
is a relatively underutilized construct.
Recall that with the term expression, we mean an instruction (such as a literal value, a method invocation, an operation, etc.) that returns a value. So, a switch
expression is a construct that returns a value. Let’s rewrite the previous example that made use of the arrow notation with a switch
expression:
Month month = getMonth(); String season = switch(month) { case DECEMBER, JANUARY, FEBRUARY -> "winter"; case MARCH, APRIL, MAY -> "spring"; case JUNE, JULY, AUGUST -> "summer"; case SEPTEMBER, OCTOBER, NOVEMBER -> "autumn"; };
There are small but important differences compared to the example of the switch
used as a statement seen in the previous section. The main difference is that this switch
expression returns a value that is assigned directly to the season
variable. The returned value is the literal pointed to by the arrow notation. Recall that in the example of the previous section the arrow notation was followed each time by the assignment of the literal to the variable season
. With the switch
expression, on the other hand, we can see an even less verbose syntax that prevents us from rewriting the assignments. Finally, we note that, being an expression, it must end with a semicolon symbol.
The yield
word
The arrow notation can therefore point to an expression that in our example was a literal, but it can also point to a block of code that perhaps contains several instructions. In this case
, to return a value from the code block we can use the word yield
. With it we can specify a value to return. For example, the following switch
expression is equivalent to the one presented in the previous example:
var season = switch(month) { case DECEMBER, JANUARY, FEBRUARY -> { String value = "winter" ; yield value; } case MARCH, APRIL, MAY -> "spring" ; case JUNE, JULY, AUGUST -> { String value = "summer" ; yield value; } case SEPTEMBER, OCTOBER, NOVEMBER -> "autumn" ; };
Note, that as expressions we used simple literals (spring and autumn), while in the code blocks we used the new syntax with the yield
statement, which allows us to return a value. In practice, a yield
statement in a switch
expression has the same role as a return
statement in a method.
From a
switch
expression, it is possible to exit only with a value, or by throwing an exception. This means that within aswitch
expression it is not possible to use thereturn
,break
orcontinue
statements.
FUN FACT: in version 12 of Java, when switch
expressions were introduced as feature preview, the word break
was used instead of the word yield
. Only in version 13 the word break
was replaced by the word yield
. In fact, the feedback for this feature preview judged the use of the break
to be misleading, as it is already used in other contexts.
FUN FACT 2: Note that we also used the word var
, and that although the compiler usually relies on the left side of the assignment (LHS) to validate the correctness of a switch
expression, when we use the word var
it is based on the right of the assignment (RHS). For more information on the word var
you can read this article.
Arrow vs Colon
Even with the switch
used as an expression, it is actually still possible to use fall through. In fact, there is an alternative syntax to the one we saw in the first example, where the arrow notation -> pointed the value to be returned (which can be defined by an expression, or by a block of code). Actually, we can still replace the arrow notation ->, with the notation we used with the ordinary switch
(aka colon :). We have also seen that the yield
statement in a switch
expression can then specify the value to be returned:
String season = switch(month) { case DECEMBER, JANUARY, FEBRUARY: yield "winter"; case MARCH, APRIL, MAY: yield "spring"; case JUNE, JULY, AUGUST: yield "summer"; case SEPTEMBER, OCTOBER, NOVEMBER: yield "autumn"; };
With this syntax, yield
can also be used outside a block of code, and is the only way to return a value. In fact, only with the arrow notation it is possible to return a literal directly. The most important difference between the two types of syntax, however, is that with the latter it is still possible to use the fall through (even if there is no need for it). In fact, the following syntax is perfectly valid:
String season = switch(month) { case DECEMBER: case JANUARY: case FEBRUARY: yield "winter"; case MARCH, APRIL, MAY: yield "spring"; case JUNE, JULY, AUGUST: yield "summer"; case SEPTEMBER, OCTOBER, NOVEMBER: yield "autumn"; };
Recall that we have already seen in the previous paragraph, that even with the syntax that makes use of the arrow notation ->
it is possible to use the yield
statement, but only within code blocks. For example, this code is valid and equivalent to the others examples seen:
String season = switch(month) { case DECEMBER, JANUARY, FEBRUARY -> {yield "winter";} case MARCH, APRIL, MAY -> {yield "spring";} case JUNE, JULY, AUGUST -> {yield "summer";} case SEPTEMBER, OCTOBER, NOVEMBER -> {yield "autumn"}; };
String season = switch(month) { case DECEMBER, JANUARY, FEBRUARY: yield "winter"; case MARCH, APRIL, MAY: yield "spring"; case JUNE, JULY, AUGUST: yield "summer"; case SEPTEMBER, OCTOBER, NOVEMBER -> "autumn"; };
would cause the following compile-time error:
error: different case kinds used in the switch case SEPTEMBER, OCTOBER, NOVEMBER -> "autumn" ; ^ 1 error
Exhaustiveness
A switch
expression has among its characteristics that of exhaustiveness. This means that the compiler will not accept situations where a possible case
clause is absent. For example, let’s modify the TrafficLight
class defined previously, so that it has a state
variable, which is set using a switch
expression. Voluntarily, however, we do not insert the Red case
:
public class TrafficLight { public String changeColor(Color lightColor) { switch(lightColor) { case GREEN -> System.out.println("The light is green"); case YELLOW -> System.out.println("The light is yellow"); // case RED -> System.out.println("The light is red"); } } }
we would get the following error when compiling:
javac TrafficLight.java TrafficLight.java:4: error: the switch expression does not cover all possible input values stato = switch(lightColor) { ^
which warns us that not all cases have been covered by the construct.
If we had used the switch
as a statement, the code would have been compilable. So, exhaustiveness is a characteristic only of switch
expressions.
To have the previous file compiled, it would be enough to re-enable the
RED case
by uncommenting the relative line. Note, however, that the above error will be reported by the compiler only because we are using an enumeration (Color
). In fact, when compiling the TrafficLight.java file, the compiler can check the enumeration to evaluate what all its elements are. However, this would not have been possible if instead of an enumeration we had had a string, an integer or a wrapper type. In fact, in these cases, we do not have a finite number of values to be assigned, and therefore the only way to cover all cases is to add adefault
clause. Therefore, in aswitch
expression, thedefault
clause should always be used, except in thecase
of using an enumeration as a value to be tested.
default
clause and exhaustiveness
Referring to the previous example, obviously nobody forbids us to use a default
clause in place of the missing RED case
, but this solution, only in the case of enumerations, could be harmful! It could also create problems if we want to add both clauses (RED case
and default
) as below:
public void changeColor(Color lightColor) { stato = switch(lightColor) { case GREEN -> System.out.println("The light is green"); case YELLOW -> System.out.println("The light is yellow"); case RED -> System.out.println("The light is red"); default -> "Unexpected case"; }; }
In fact, suppose that the Color
enumeration evolves to define the BLACK
color, which will be used to manage situations in which the traffic light is off:
public enum Color { GREEN, YELLOW, RED, BLACK; }
When we compile, the default
clause would prevent the compiler from warning us that we are not covering all possible cases and we will only discover the problem at runtime. So, by launching the following test class:
public class TrafficLightTest { public static void main(String args[]) { TrafficLight trafficLight = new TrafficLight(); trafficLight.changeColor(Color.RED); trafficLight.printState(); trafficLight.changeColor(Color.YELLOW); trafficLight.printState(); trafficLight.changeColor(Color.GREEN); trafficLight.printState(); trafficLight.changeColor(Color.BLACK); trafficLight.printState(); } }
We will obtain the following output:
The light is red The light is yellow The light is green Unexpected case
Conclusion
With Java 14 we have a renewed switch
construct that is much more interesting than the one we have used for many years. In particular, the arrow notation, and the possibility of grouping the values of several cases, separating them with commas, guarantees us less verbosity and greater robustness of our code. We can still use the classic colon notation (and consequently the fall through technique) for compatibility reasons with code written with previous versions of Java, however the old syntax will probably be abandoned in the future. The word yield
allows you to return a value in switch
expressions, just as the word return
allows you to return a value in methods. Finally, the concept of exhaustiveness of switch
expressions grants us a compile-level check of the completeness of the definition when we use enumerations. The new switch
therefore represents another fundamental reason for going beyond Java 8.
Author’s Notes
This article is based on a few paragraphs from my English book “Java for Aliens“.