Switch Expressions in Java 14 add Simplicity and Agility

[article]
Summary:

The article discusses how the new switch statement is simplified and how the new switch expression simplifies. After setting the environment, we’ll discuss what was lacking in the switch statement that makes it less agile. Then, we’ll discuss how Java 14 simplifies switch.

 

The switch statement in Java is a control-flow statement used to test if a variable or an expression value matches a value specified by one of the case labels or the default label in the switch block denoted by {}. All statements following a matching case label are run.

Two of the principles in the Agile Manifesto are about software design and simplicity: "... good design enhances agility." and "Simplicity--the art of maximizing the amount of work not done..."
 
The switch expressions and a simplified switch statement in Java 14 add simplicity, an improved design for switch, and as a result, agility.

We’ll first discuss how the new switch statement is simplified and how the new switch expression simplifies. After setting the environment, we’ll discuss what was lacking in the switch statement that makes it less agile. Then, we’ll discuss how Java 14 simplifies switch.

Setting the Environment

Download Java SE Development Kit 14. Extract the zip file, and copy the contents of the innermost directory to a directory (C:/jdk14 as an example). Add or update the environment variables discussed in Table 1.
Table 1: Environment Variables for JDK 14

Environmental VariableDescriptionValue Added
JAVA_HOMESpecifies Java Home directory.C:\jdk14
PATHAdds the Java Home bin directory to PATH.C:\jdk14\bin
CLASSPATHAdds the directory to classpath. Java applications may be copied to this directory for running.C:\;C:\jdk14


Run the following command to output the Java version, which should be 14.

java –version


The Problem

The traditional switch statement makes use of fall-through semantics in that all statements following a matching case label are run unless a break statement is used to break out of the switch statement. This means all statements in subsequent case labels following a matching case label are run even though just one case label is matched. The fall-through semantics make it necessary to provide a break statement with each case label code block to avoid the statements in all subsequent case labels from running. As a result, the traditional switch statement has the following shortcomings:

  • Unneeded statements get run.

  • The default scoping of a switch block is the complete block. Same variable name cannot be used in statements associated with multiple case labels.

  • The switch may only be used as a statement even though it could be necessary to use a switch expression in which a variable is assigned the expression value.

  • Multiple break statements have to be added to break the fall-through semantics in a switch block.


As an example of the traditional switch statement, consider the following application SwitchTraditional that is used to find if a specific enum value is a LTS version or a Non-LTS version of Java. In the application the switch block falls through after the matching case label, and all subsequent statements are run.  Further, because the default scoping of the switch block is the complete block, we have to declare multiple variables with slightly different names; version, version2 and version3.

package jdk; 

public class SwitchTraditional {

  public enum JAVA_VERSION {
    JavaSE6,JavaSE7,JavaSE8,JavaSE9,JavaSE10,JavaSE11,JavaSE12,JavaSE13
}

static JAVA_VERSION version=JAVA_VERSION.JavaSE7;

public static void main(String[] argv){ 
 
switch (version) {
            case JavaSE6:
                System.out.println("Does not apply");
                break;
            case JavaSE7:
                System.out.println("Does not apply");
                
            case JavaSE8:
                System.out.println("Does not apply");
                
            case JavaSE9:
                System.out.println("Non-LTS");
                 
            case JavaSE10:
                System.out.println("Non-LTS");
                
            case JavaSE11:
                String version="JAVASE11";
                System.out.println(version+" is LTS");
                 
            case JavaSE12:
                String version2="JAVASE12";
                System.out.println(version2+" is Non-LTS");
            case JavaSE13:
                 String version3="JAVASE13";
                System.out.println(version3+" is Non-LTS");
            default:
                System.out.println("Does not apply or Java version is not defined.");
                 
        }
}
}

Compile and run the application, and the following output is generated when in fact just one case label is matched.
java jdk.SwitchTraditional
Does not apply
Does not apply
Non-LTS
Non-LTS
JAVASE11 is LTS
JAVASE12 is Non-LTS
JAVASE13 is Non-LTS
Does not apply or Java version is not defined.
 
To avoid a fall-through in a switch block, a break statement has to be added in each case label statement block as follows.
switch (version) {
            case JavaSE6:
                System.out.println("Does not apply");
                break;
            case JavaSE7:
                System.out.println("Does not apply");
                break;
            case JavaSE8:
                System.out.println("Does not apply");
                break;
            case JavaSE9:
                System.out.println("Non-LTS");
                 break;
            case JavaSE10:
                System.out.println("Non-LTS");
                break;
            case JavaSE11:
                String version="JAVASE11";
                System.out.println(version+" is LTS");
                 break;
            case JavaSE12:
                String version2="JAVASE12";
                System.out.println(version2+" is Non-LTS");
                 break;
             case JavaSE13:
                 String version3="JAVASE13";
                System.out.println(version3+" is Non-LTS");
         break;  
            default:
                System.out.println("Does not apply or Java version is not defined.");
                 }
And if a variable called version were to be declared more than once in the switch statement. the following error message would result.
  error: variable version is already defined in method main(String[])

The Solution

Java SE 14 introduces a new language feature for switch statement. The new language feature for switch does not implement fall-through semantics and makes use of a new form of switch label denoted as follows:
case L ->

Only the code to the right of a matching case L -> switch label is run, and the switch block does not fall-through to run subsequent statements. The right of a switch label may only be one of the following:

  • An expression

  • A block denoted with {}

  • A throw statement


In addition to the non-fall-through semantics a switch expression is supported. A switch expression has the following syntax.
T result = switch (arg) {
    case L1 -> e1;
    case L2 -> e2;
    default -> e3;
};

A switch expression has a value that is assigned to the variable result in the preceding example. To demonstrate the non-fall-through semantics of the new simplified switch statement, consider the following application in which a switch statement makes use of the new form of the switch label and is invoked with a specific value once.

package jdk;

public class SwitchExpression {

  public static void main(String[] argv){
version(9);
}
static void version(int version) {
    switch (version) {
        case 9 -> System.out.println("JDK9\n");
        case 10 -> System.out.println("JDK10\n");
        case 11 -> System.out.println("JDK11\n");
         case 12 -> System.out.println("JDK12\n");
        case 13 -> System.out.println("JDK13\n");
    }
  }
}
  
Compile and run the application, and the switch block does not fall-through to run all statements. Only the code to the right of the matching case label is run with the following output.
JDK9
Next, we shall discuss the new simplified switch statement and switch expression in more detail.

Simplified Form of Switch Statement

In the new form of switch label case L ->  multiple comma separated case labels may be specified in a single switch label. In the following application SwitchExpression,which is a variation of the application used in the The Problem section, multiple case labels are specified in some of the switch labels.  If any of the case labels in a multi-case switch label is matched, the code to the right of -> in the matching switch label and only the code to the right of -> is run.

package jdk; 

public class SwitchExpression{

  public enum JAVA_VERSION {
    JavaSE6,JavaSE7,JavaSE8,JavaSE9,JavaSE10,JavaSE11,JavaSE12,JavaSE13
}
static JAVA_VERSION version=JAVA_VERSION.JavaSE9;
public static void main(String[] argv){
  
switch (version) {
    case JavaSE6, JavaSE7, JavaSE8 -> System.out.println("Does not apply");
    case JavaSE9,JavaSE10,JavaSE12,JavaSE13 ->  System.out.println("Non-LTS");
    case JavaSE11 ->  System.out.println("LTS");
    default -> System.out.println("Does not apply or Java version is not defined.");
}
}
}

Compile and run the application with the following single output instead of multiple outputs generated by a traditional switch statement in which fall-through semantics are used.
Non-LTS
The code to the right of -> could be a block denoted by {} as in the following switch statement in which local variables declared in any of these blocks are scoped only to the block itself and not the complete switch block. To demonstrate, we have declared the same variable name version in two switch label blocks.

switch (version) {
    case JavaSE6, JavaSE7, JavaSE8 -> System.out.println("Does not apply");
    case JavaSE9,JavaSE10,JavaSE12,JavaSE13 -> {String version="JavaSE9 or JavaSE10 or JavaSE12 or JavaSE13"; System.out.println(version+" is Non-LTS");}
    case JavaSE11 -> {String version="JAVASE11"; System.out.println(version+" is LTS");}
    default -> System.out.println("Does not apply or Java version is not defined.");
}
The same application SwitchExpression run with the preceding switch statement generates the following output. 
JavaSE9 or JavaSE10 or JavaSE12 or JavaSE13 is Non-LTS

Using a Switch Expression

In this section we’ll demonstrate switch control-flow construct as an expression—a switch expression. Using the same application SwitchExpression, add a switch expression in an assignment statement as follows.
String support = switch (version) { 
    case JavaSE6, JavaSE7, JavaSE8 -> "Does not apply";
    case JavaSE9,JavaSE10,JavaSE12 ,JavaSE13 -> "Non-LTS";
    case JavaSE11     -> "LTS";
    default -> "Does not apply or Java version is not defined";
};
Output the value of the switch expression assigned to a variable.
System.out.println(support);
Compile and run the application with the same output as before.
Non-LTS
The target-type of switch expression does not have to be declared or known. The type of a switch expression is its target-type, if known. If the target-type of the switch expression is known, as we have declared it to be String in the assignment statement, it is supplied to each of the switch label evaluation paths denoted by case L->.  The value that a statement, expression, or block in a specific switch label evaluation path denoted by case L-> evaluates to must be of the type or compatible with the type of the switch expression. To demonstrate, modify the switch expression to make the value of one of the switch label evaluation paths to be integer 5, which is not the same as or compatible with the switch expression type of String.
String support = switch (version) { 
    case JavaSE6, JavaSE7, JavaSE8 -> "Does not apply";
    case JavaSE9,JavaSE10,JavaSE12 ,JavaSE13 -> "Non-LTS";
    case JavaSE11     -> "LTS";
    default -> 5;
};
When the application is run, the following output is generated.
error: incompatible types 
    default -> 5;
               ^
    int cannot be converted to String
1 error
If the type of the switch expression is not known, a standalone type is computed by combining the types of each of the switch label evaluation paths denoted by case L->.
Using Reserved Type Var with a Switch Expression
A switch expression is a poly expression, and in a local variable declaration in which the value of a switch expression is used as the variable initializer, the reserved type name var may be used. Using the same switch expression as before the var reserved type is used as follows.
var support = switch (version) { 

    case JavaSE6, JavaSE7, JavaSE8 -> "Does not apply";
    case JavaSE9,JavaSE10,JavaSE12 ,JavaSE13 -> "Non-LTS";
    case JavaSE11     -> "LTS";
    default -> "Does not apply or Java version is not defined";
};
System.out.println(support);
Compile and run the application to generate the same output.
Non-LTS
If the reserved type var is used in a local variable declaration with a switch expression as the initializer, the target-type of the switch expression is not known and is inferred from the switch label evaluation paths denoted by case L->.  Each of the switch label evaluation paths denoted by case L-> may evaluate to the same or a different type. As an example, two of the multi-case switch labels  evaluate to String type, one to type int and another to a floating-point type. 
var support = switch (version) { 

    case JavaSE6, JavaSE7, JavaSE8 -> 1;
    case JavaSE9,JavaSE10,JavaSE12 -> "Non-LTS";
    case JavaSE11     -> "LTS";
    default -> 1.8;
};

The output with the preceding switch statement is as follows.
Non-LTS

Using the Yield Statement  

Switch also introduces the yield statement to yield a value. Each of the switch labels (single case label or multi-case label) denoted by case L-> expression or statement or block or throw must evaluate to a value. If a block denoted by {} is used with case L->  it must evaluate to a value, which makes it necessary to add support for a yield statement that takes a value as an argument with following syntax in which value is a variable.

yield value;

The yield statement yields a value, which becomes the value of the enclosing switch expression. To demonstrate the new form of yield statement use a block in one of the case L-> evaluation paths as in the following application. In the block, add a yield statement with a value that becomes the value of the switch expression if the switch label is matched. In the following example application SwitchExpression, the version variable is set to a value so that the default case, which declares a block with a yield statement to yield a value, is matched. 

package jdk; 

public class  SwitchExpression{

  public enum JAVA_VERSION {JavaSE5,JavaSE6,JavaSE7,JavaSE8,JavaSE9,JavaSE10,JavaSE11,JavaSE12,JavaSE13
}

static JAVA_VERSION version=JAVA_VERSION.JavaSE5;

public static void main(String[] argv){


var support = switch (version) { 

    case JavaSE6, JavaSE7, JavaSE8 -> 1;
    case JavaSE9,JavaSE10,JavaSE12,JavaSE13 -> "Non-LTS";
    case JavaSE11     -> "LTS";
    default -> { String ver=version.toString(); yield ver; }
};

System.out.println(support);
}
}
Output from running the application is:
JavaSE5
A switch expression does not have to make use of the new syntax for denoting a switch label case L-> and may be used with the traditional syntax of case L : and the yield statement with a value may be used with the traditional syntax case L : as follows, in the SwitchExpression class.

var support = switch (version) { 

    case JavaSE6, JavaSE7, JavaSE8 : yield "Neither LTS nor Non-LTS";
    case JavaSE9,JavaSE10,JavaSE12,JavaSE13 : yield "Non-LTS";
    case JavaSE11: yield "LTS";
    default : String ver=version.toString(); 
              yield ver; 
};

System.out.println(support);
Compile and run the application to get the following output.
JavaSE5
The different syntax for case labels case L: and case L-> cannot be mixed. To demonstrate, modify the switch expression as follows in which both kinds of case labels are made use of.

var support = switch (version) { 

    case JavaSE6, JavaSE7, JavaSE8 : yield "Neither LTS nor Non-LTS";
    case JavaSE9,JavaSE10,JavaSE12,JavaSE13 : yield "Non-LTS";
    case JavaSE11: yield "LTS";
    default -> { String ver=version.toString(); yield ver; }
};

Compile the application and an error message gets generated.
error: different case kinds used in the switch
    default -> { String ver=version.toString(); yield ver; }
    ^
1 error

A Switch Expression Must Cover All Possible Input Values  

A switch expression must specify a case label for all possible input values. As it is practically not feasible to specify a case label for each possible value of a primitive type such as int, or String type, or any of the other types, a default case would invariably need to be included in a switch expression. To demonstrate, consider the following application with a switch expression in an assignment statement with target type as String.
package jdk; 

public class  SwitchExpression{

 static String  version="JavaSE10";

public static void main(String[] argv){


String support = switch (version) { 

    
    case "JavaSE10" -> "JavaSE10";
     
    default -> "JavaSE8";
};

System.out.println(support);
}

}

As the default case is included in the switch expression, the application compiles and runs without an error message and with the following output.
 
JavaSE10
Next, remove the default case in the switch expression.
String support = switch (version) { 
    case "JavaSE10" -> "JavaSE10";
   // default -> "JavaSE8";
};
Compile the application, and an error message gets generated.
error: the switch expression does not cover all possible input values
String support = switch (version) {
                 ^
1 error
For an enum switch expression that covers all known cases a default case may be omitted as in the following application. 
package jdk; 

public class  SwitchExpression{

public enum JAVA_VERSION {
    JavaSE5,JavaSE6,JavaSE7,JavaSE8,JavaSE9,JavaSE10,JavaSE11,JavaSE12,JavaSE13
}

static JAVA_VERSION version=JAVA_VERSION.JavaSE10;

public static void main(String[] argv){


var support = switch (version) { 

    case JavaSE5,JavaSE6,JavaSE7,JavaSE8,JavaSE9 -> JAVA_VERSION.JavaSE9;
    case JavaSE10 -> JAVA_VERSION.JavaSE10;
    case JavaSE11     -> JAVA_VERSION.JavaSE11;
     case JavaSE12     -> JAVA_VERSION.JavaSE12;
     case JavaSE13     -> JAVA_VERSION.JavaSE13;
 
};

System.out.println(support);
}


}

Compile and run the application, and it compiles and runs without generating an error message and with the following output.
 
JavaSE10
   
But if all possible input values are not covered with an enum type switch expression, an error message is generated as in the following application.

package jdk; 

public class SwitchExpression{

  public enum JAVA_VERSION {
    JavaSE5,JavaSE6,JavaSE7,JavaSE8,JavaSE9,JavaSE10,JavaSE11,JavaSE12
}

static JAVA_VERSION version=JAVA_VERSION.JavaSE10;

public static void main(String[] argv){


var support = switch (version) { 

    case JavaSE9 -> JAVA_VERSION.JavaSE9;
    case JavaSE10 -> JAVA_VERSION.JavaSE10;
    case JavaSE11     -> JAVA_VERSION.JavaSE11;
     // default -> JAVA_VERSION.JavaSE8;
};

System.out.println(support);
}
}

Compile the application and an error message gets generated.
error: the switch expression does not cover all possible input values
var support = switch (version) {
              ^
1 error
 
A Switch Expression Must Complete Normally with a Value or Throw an Exception

A switch expression, whether making use of the traditional case L: syntax or the new case L-> syntax, must complete normally or abruptly. A switch expression is said to complete normally if it computes a value.  A switch expression is said to complete abruptly if it throws an exception. To demonstrate the requirement for completing normally with a value, do not include a yield with a value in one of the case labels as in the following application.
package jdk; 

public class  SwitchExpression{

public enum JAVA_VERSION {
    JavaSE5,JavaSE6,JavaSE7,JavaSE8,JavaSE9,JavaSE10,JavaSE11,JavaSE12,JavaSE13
}

static JAVA_VERSION version=JAVA_VERSION.JavaSE5;

public static void main(String[] argv){


var support = switch (version) { 

    case JavaSE6, JavaSE7, JavaSE8 : yield "Neither LTS nor Non-LTS";
    case JavaSE9,JavaSE10,JavaSE12,JavaSE13 : yield "Non-LTS";
    case JavaSE11: yield "LTS";
    default : String ver=version.toString(); 
                
};

System.out.println(support);
}
}
Compile the application, and an error message is generated. 
error: switch expression completes without providing a
 value
};
^
  (switch expressions must either provide a value or throw for all possible input values)
1 error

A switch expression may specify a throw statement as an alternative to providing a value. To demonstrate, consider a slight variation of the preceding application in which the default label statement is a throw statement. Invoke the switch expression with an input value not covered in any of the named case labels as a result matching the default case statement. The switch expression is enclosed in a try/catch statement.
package jdk; 

public class  SwitchExpression{

  public enum JAVA_VERSION {
    JavaSE5,JavaSE6,JavaSE7,JavaSE8,JavaSE9,JavaSE10,JavaSE11,JavaSE12,JavaSE13
}

static JAVA_VERSION version=JAVA_VERSION.JavaSE5;

public static void main(String[] argv){

try{
var support = switch (version) { 
    case JavaSE6, JavaSE7, JavaSE8 : yield "Neither LTS nor Non-LTS";
    case JavaSE9,JavaSE10,JavaSE12,JavaSE13 : yield "Non-LTS";
    case JavaSE11: yield "LTS";
    default : throw new Exception("Version not defined");
                
};
System.out.println(support);
}catch(Exception e){System.out.println(e.getMessage());}
 
}

}
Compile and run the application and the exception message thrown in the switch expression gets output.
 
Version not defined
 
Control Statements Continue, Return and Break Cannot Jump Through a Switch Expression

Because a switch expression must complete normally with a value or throw an exception, control-flow statements continue, return,  and break cannot jump through a switch expression. To demonstrate, specify a return statement  in the default case statement block of a switch expression.   
   
package jdk; 

public class SwitchExpression{

 public static void main(String[] argv){
  
  version(12);
 
}

static int version(int version) {
  int ver=  switch (version) {
        case 9 -> {yield 9;}
        case 10 -> {yield 10;}
        case 11 -> {yield 11;}
         default -> {return 12;}
    };
     return ver;
}
}
Compile the application, and an error message gets generated.

error: attempt to return out of a switch expression
         default -> {return 12;}
                     ^
1 error

Summary 

In this article we discussed the new simplified form of the switch statement and the switch expressions introduced as a new language feature in JDK 14. The simplified switch makes use of a new form of switch label case L-> that does not use fall-through semantics, and only the code to the right of ->  is run. Switch expressions and the simplified switch statement add simplicity and agility to Java.

About the author

AgileConnection is a TechWell community.

Through conferences, training, consulting, and online resources, TechWell helps you develop and deliver great software every day.