You are currently viewing The new switch (switch expressions)

The new switch (switch expressions)

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 cases, 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 a switch expression it is not possible to use the return, break or continue 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 a default clause. Therefore, in a switch expression, the default clause should always be used, except in the case 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“.

 

Leave a Reply