Here are the highlights of changes to
switch:
It was added in Java 12 as a preview feature. It stayed as a preview feature in Java 13 with a minor change – the break with value in a switch expression in Java 12 was replaced with a yield statement in Java 13.switch has received new syntax. The new switch syntax allows you to use an arrow (->) instead of a colon (:) to separate the case label from its corresponding code. In my book, I call them as switch-with-colon and switch-with-arrow.
Now you can also use multiple constants in one case label.You can use both switch-with-colon and switch-with-arrow as a statement and an expression. Before this change, you had only switch-with-colon and you could use it only as a statement.To support enhancements to switch, there is a new yield statement, which can be used in a switch expression. This may break your existing code, which would be easy to fix though. More details on this in a separate section below.
Let me give you an example of all changes with a little explanation. Some people do not know what a preview feature is, so I will start with explaining it first.
What Is a Preview Feature?
A preview feature of the Java SE Platform is a fully specified and fully implemented language or virtual machine feature, which is available in a JDK feature release, but is not a permanent feature of the JDK yet. A preview feature is provided to solicit developers feedback. Developers are expected to experiment with the features in real world use-cases. Based on the feedback, a preview feature may become permanent (standard feature) in a future release with or without modifications or it may be removed altogether. A preview feature is not meant to be used in production. Refer to JEP 12 at
https://openjdk.java.net/jeps/12 for more details on preview features.
A preview feature is not backward compatible. You cannot run class files that contain preview features from Java 12 using Java 13 runtime.
Since a preview feature is not meant to be used in production, it is not enabled in the compiler and runtime by default. You must use the
--enable-preview option with the compiler and runtime to use a preview feature. You will need to use the
--release or
-source option when you use the
--enable-preview option to compile the source code. To compile source code with preview features in JDK 13, you would use:
If you do not use the
--enable-preview option to compile the source code with preview features, you receive compile-time errors similar to the following:
If you enable preview features at compile-time, the compiler prints notes suggesting you to compile with a
-Xlint:preview option, so you can see the warnings.
To run compiled classes with preview features, you specify the
--enable-preview option with the
java command:
If you want to use JShell to explore preview features, you need to start it with the
--enable-preview option:
Enhancements to Traditional Switch
Java has always been backward compatible. Adding breaking changes to the traditional switch was not an option to the language designers. The preview feature provides you new ways of using the traditional switch without breaking your old code. Here are the changes to the traditional switch:
You can have multiple constants in a case label.You can use switch as an expression.
Previously, you had to use one case label per constant. The new
switch syntax allows you to use a comma-separated list of constants in a case label. The following snippet of code uses the traditional
switch with multiple constants in a
case label to match a letter against a vowel and consonant:
With the new
switch syntax, you have collapsed five
case labels into one. You still need to use a
break statement because the traditional
switch statement still uses default fall-through semantics.
The preview feature allows you to use the traditional switch as an expression. An expression evaluates to a value (or yields a value). Now you can write code like:
Notice the use of a semicolon at the end of the
switch expression. The
switch expression is part of the variable declaration for the
str variable, so the entire variable declaration must end with a semicolon. Think of the previous variable declaration as shown:
The traditional
switch allowed you to use only statements for a
switch label. The preview feature has introduced a new
yield statement. Its syntax is:
yield expression;
The
yield statement evaluates its expression and transfers control to the enclosing
switch expression. The value of the expression becomes the value of the
switch expression.
Tip The
yield statement can only be used inside a
switch expression. You cannot use the
break statement inside a
switch expression.
Here is an example of a
switch expression using the traditional
switch:
This is a simple example where each
switch label contains only one
yield statement. You can have multiple statements in a
switch label. However, each
switch label must contain one
yield statement that will represent the value of the
switch expression. Typically, you will not print something from an expression. Let us do it just for the demonstration purpose to show that you can have multiple statements in a
switch label for a switch expression. The following snippet of code prints a message when
"case 1:" is executed:
Recall that you cannot use the
break statement inside a
switch expression. The
yield statement completes a
switch expression. In this statement, when
count is 1, a message is printed, and the
yield statement completes the
switch expression by yielding "One" and other
case labels below
"case 1:" are not executed.
Here is a challenge for you. Let me switch the two statements in the previous snippet of code as follows. Now, the
yield statement is first and the
System.out.println() is second. Will the following snippet of code compile?
If your answer is no, you are correct. The
yield statement completes the
switch expression making the
System.out.println() statement unreachable. Java does not allow unreachable statements. This snippet of code generates the following compile-time error:
New Switch Syntax
The new
switch retains most of the syntax from the traditional
switch, except the character to separate a
switch label from its code. It uses an arrow (->) instead of a colon (:). The traditional
"case label:" becomes
"case label->". This is the only syntactic difference you have for the new switch. There are several semantic differences that I explain in this section.
Often, I need to distinguish which
switch I am talking about–the new one or the old new. I refer to the old
switch as traditional
switch,
switch-with-colon, or
switch with
"case label:". I refer to the new one as new
switch,
switch-with-arrow, or
switch with "case label->". Java Language Specification refers to the
switch block associated with the traditional
switch as
switch labeled statement groups and the
switch block associated with the new
switch as
switch labeled rules. Whichever name you use to refer to the new
switch, here is its syntax:
The syntax allows for one or more constants in a
case label. The code associated with a
switch label is restricted to one of the following: an expression, a block statement, and a
throw statement. I will explain the rationale behind this rule shortly.
Let us look at the rules used to process the new
switch followed with examples of each rule:
The default is no fall-through in the new switch. This means, you no longer need to use a break statement to stop executing the switch labels following the matched switch label.Only one "thing" can be executed as part of a matched switch label. That "thing" can be an expression, a block statement, or a throwstatement. In traditional switch, a group of statements was allowed causing the scoping issue where the entire switch block was executed in one scope. Using these constraints, if you have more than one statement to execute, you are forced to use a block statement that will have a new scope for the switch label. This allows you to have the same functionality as the traditional switch, but code is less error-prone.The new switch can be used as a statement or as an expression.
The following snippet of code rewrites the previous example, which used the traditional
switch statement, using the new
switch statement:
Notice the use of the
System.out.println() in each switch label. Didn't I say that each switch label can have an expression, a block statement, or a
throw statement? I did say that. In Java, a method call (in this case,
System.out.println()) is an expression. When you add a semicolon after the method call, it becomes an expression statement. Here are the detailed rules about using an expression in a
switch label with arrow:
In a switch statement, the expression must be an expression statement–an expression, which can be converted to a statement by adding a semicolon to it.In a switch expression, the expression may be any valid Java expression.
The following snippet of code rewrites the previous example using a
switch expression:
This time, "One", "Two", etc. are expressions. You cannot convert them to expression statements by adding a semicolon. When the
switch expression (
count) matches one of the
case labels, the corresponding expression ("One", "Two", etc.) becomes the value for the
switch expression.
If you have to compute the value in a
switch label using some logic, you will need to use the
yield statement to return the value as the value for the
switch expression. Consider the following snippet of code that uses a
switch expression to compute a value:
The
case label uses a simple expression "Vowel" when the character is a vowel. The default label uses a block statement to use logic to determine whether it is a consonant or not a letter. Note the use of the
yield statements inside the
if and
else blocks. The
default label does not use very complex logic. You can replace it with an expression as shown:
Consider the following snippet of code that uses the new switch statement:
The code uses one
case label and one
default label. Both contain more than one statement, so you are forced to use a block statement ({}) in each label. Notice that you have used the same variable name
upperChar inside the code for both labels. Declaring duplicate variables in a
switch block like this was also possible with traditional
switch, but you had to remember to use a block statement, whereas the new
switch forces you to use a block statement.
Does New switch Replace the Old One?
Once the new
switch becomes a standard feature in Java SE (maybe in Java SE 14), you are encouraged to use it in place of the traditional
switch when it fits your needs. However, it is not a complete replacement for the traditional
switch statement. Remember that the traditional
switch offers fall-through by default and you have a way (using the
break statement) to override the default behavior. Consider the following trivial example that uses the fall-through feature of the traditional
switch statement:
If the value of count is 1, it prints "One Two Three"; for 2, it prints "Two Three"; for 3, it prints "Three"; and, for any other values, it prints "Over-My-Head". Implementing this logic is simple because of the fall-through feature of the traditional
switch statement. There is no straightforward way to implement this logic using the new
switch syntax because the new
switch does not provide the fall-through feature. You got the point–use the new syntax whenever possible and use the traditional
switch when fall-through is needed.
I was curious to get a bit more insight into the new
switch features and how the compiler handles them. So, I tried to decompile all my examples, which were written using new
switch syntax. You can use
http://www.javadecompilers.com to decompile your Java code. When I decompiled my code, I did not see any new
switch syntax. I found out that in all cases, the code using new
switch syntax was translated to the traditional
switch. Even though the new
switch uses the old
switch behind the scene, as a developer you gain a lot of benefits using the new syntax. You get compact and less error-prone code, and a new big feature, which is using
switch as an expression!
A Switch Expression is a Poly Expression
A poly expression in Java is an expression whose type depends on the context. Therefore, the same poly expression can take on different types in different contexts. A
switch expression is a poly expression. If its target type is known, its type is the type of its target type. If its target type is unknown, its type is computed by combining the types of each
switch labels. Consider the following snippet of code:
This
switch expression has a target type of
double, which is the type of the
value variable. Therefore, the type of the
switch expression is
double. The compiler also checks that the type of the value yielded from each
switch label is assignment compatible to the target type of the
switch expression. In this case, the
switch labels yields an
int (10), a
double (20.4) and a
float (1.5F). All three types,
int,
double, and
float, are assignment compatible to the target type
double.
Consider the following snippet of code. Will it compile?
This snippet of code does not compile. The target type of the
switch expression is
int, which is the declared type of the
value variable. The
double and
float values yielded from the two
switch labels are not assignment compatible to
int. This is the reason why this code does not compile.
Consider the following snippet of code. Will it compile?
The code compiles fine. Notice the use of
var to declare the
value variable. This time, the target type of the
switch expression is unknown. The compiler has to compute the type of the
switch expression by looking at each
switch label. The
switch labels yield values of
int,
double, and
float type. The compiler uses the type-widening rules and computes
double as the type of the
switch expression. Remember that the compiler has to infer the type of the
value variable because of
var. The type value will be inferred as
double, which is the same as the computed type of the
switch expression.
Consider the following snippet of code. Will it compile?
If you guessed that this snippet of code won't compile, you are wrong. It compiles fine. Now you might be curious to know the computed type of the
switch expression. The compiler uses the following steps:
There are three types of values yielded from switch labels: int, double, and String. The types are mixed–two primitive types and one reference type.The primitive types are promoted to reference types Integer and Double.The compiler looks at the common types among Integer, Double, and String. The compiler computes a type that is the union of all common types of these three types. All three types implement four interfaces: Serializable, Comparable, Constable, and ConstantDesc. The computed type of the switch expression is a combination of these four interfaces.
So, what is the type of the
switch expression in the previous snippet of code? Here it is:
This is a non-denotable type. That is, only the compiler can use this type in bytecode. You cannot use (or denote) this type in your source code. To see this for yourself, you can use JShell to run this snippet of code. Make sure to use the
--enable-preview option to start Jshell and set the feedback mode to
verbose, as shown:
The yield Statement
The new
switch expression has introduced a new statement called the
yield statement. Making
yield a statement has some implications on the existing code. It might break existing code if you had a
yield() method, which was invoked using unqualified syntax. Let us take an example to understand this rule. Consider the followings yield statement:
Before
yield being a statement, this was definitely a method call. That is, it will call a method named
yield by passing a value 10.
After the introduction of the
switch expressions, this statement will be interpreted as a
yield statement that yields a value 10 and it is only permitted to be used in
switch expressions. Note that using parenthesis around 10 does not make this a method call because now
yield is a statement and (10) is its value. All of the following variants will be treated as a
yield statement–whitespaces and parenthesis making no difference:
If you have unqualified
yield() method calls in your existing code, you will need to modify it to make them qualified method calls. That is, if you have
yield(x) in your existing code, you will need to change it to
this.yield(x) or
C.yield(x) depending on the context.
With the introduction of the
switch expressions,
yield is a restricted identifier. Its use is restricted in certain contexts. You cannot use
yield as a type name such as a class name or an interface name. However, you can use
yield as a method name and a variable name. Using
yield as a method is allowed for backward compatibility because the
Thread class in Java contains a
yield method and, in rare cases, you might have named your methods as
yield.
The following is a complete program to show how to use
yield as a method/variable names and how to call such methods. You must use the
NoYieldAsTypeName.yield() syntax to call the
yield() method.
Output:
Mixing Switch Label Syntax
You have two syntax to declare
switch labels–one uses a colon (:) and one uses an arrow (->). In one
switch, you cannot mix the two syntax. The following snippet of code does not compile: