Home | Send Feedback

Java Switch Expression in Java 14

Published: 20. November 2021  •  java

In this blog post, we are going to take a look at a feature that has been introduced in Java 14 (March 2020), the new switch. To be precise, we got not just one but three new switch constructs one switch statement and two switch expressions.

Let's first look at the classic switch statement we have had since the beginning of Java.

Classic syntax, statement

The switch statement is a heritage from C and inherits the same semantic. It's a statement that means it has no value and can't be assigned to a variable. Another characteristic is that, by default, cases fall through.

The syntax of the switch statement hasn't changed since the first version of Java, but over time the Java language developers added new capabilities to it.
Before Java 5 switch only worked with the primitive datatypes byte, short, char, and int.
Java 5 added support for the wrapper types Character, Byte, Short, and Integer.
Java 5 also introduced enum and support for the switch to work with them.
Java 7 added the ability to switch over String.

The following code is valid syntax since Java 7.

    int number;
    String input = "ONE";

    switch (input) {
      case "ONE":
        number = 1;
        break;
      case "TWO":
        number = 2;
        break;
      case "THREE":
        number = 3;
        break;
      default:
        number = -1;
    }

Datatypes of the switch argument and the case values must be the same. You can't pass null as input to switch; the program throws a NullPointerException if that happens.

You can use variables in the case, but they must be final (constant).

    String input = "ONE";
    final String one = "ONE";
    final String two = "TWO";
    switch (input) {
      case one:
        number = 1;
        break;
      case two:
        number = 2;
        break;
      case "THREE":
        number = 3;
        break;
      default:
        number = -1;
    }

As mentioned before, the cases fall through by default unless you insert a break to exit the switch. You can use this behavior to your advantage if multiple cases need to be handled the same way.

    enum Status {
      SUBMITTED, PROCESSING, READY, ERROR
    }

    Status currentStatus = Status.PROCESSING;

    switch(currentStatus) {
      case PROCESSING:
      case READY:
        System.out.println("everything is okay");
        break;
      case ERROR:
        System.out.println("something went wrong");
        break;
    }

The break in the last case is strictly speaking unnecessary, but the compiler will not complain, and it helps prevent bugs in the future when you add more cases to the switch. It is also worth noting that the default case is not mandatory; the classic switch statement does not have to be exhaustive, which means it does not have to cover every possible input value. If you pass a value to the switch that's not covered by any of the cases and there is no default case, nothing happens. The program skips the whole switch block and continues with the code after the switch statement.


Next, we look at the three new switch constructs introduced in Java 14.

Arrow syntax, expression

The new switch changes the syntax; instead of a colon (:), it uses an arrow (->) to separate the case from the corresponding code. The whole switch block is an expression it has a value and can be assigned to a variable or used as a return argument. The most noticeable change is that cases no longer fall through. The new arrow switch syntax supports the same data types as the classic switch.

   String input = "ONE";
   int number = switch (input) {
      case "ONE" -> 1;
      case "TWO" -> 2;
      case "THREE" -> 3;
      default -> -1;
    };

Because this switch is an expression each case has to produce a value. Code like this does not compile.

   // does not compile. case "TWO" does not produce a value
   String input = "ONE";
   int number = switch (input) {
      case "ONE" -> 1;
      case "TWO" -> System.out.println("2");
      case "THREE" -> 3;
      default -> -1;
   };

The code to the right of a case may be an expression, a block, or a throw statement.

If the logic is a bit more complicated and you need to execute multiple statements in one case you must wrap the code with curly braces {} (block) and produce a value with yield.

String input = "ONE";
int number = switch (input) {
    case "ONE" -> 1;
    case "TWO" -> {
      System.out.println("computing");
      int result = 1 + 1;
      yield result;
    }
    case "THREE" -> 3;
    default -> -1;
};

yield is not a keyword. The Java specification calls it a restricted identifier (like var). You can't, for example, name a class yield, but you can use yield as a variable name. The following code compiles without any errors, although it is very confusing.

    case "TWO" -> {
      System.out.println("computing");
      int yield = 1 + 1;
      yield yield;
    }

With the new switch, the cases no longer fall through. If you have multiple cases that have to be handled the same way you list them after case comma-separated.

    enum Status {
      SUBMITTED, PROCESSING, READY, ERROR
    }

    Status currentStatus = Status.SUBMITTED;
    boolean ok = switch(currentStatus) {
      case SUBMITTED, PROCESSING, READY -> true;
      case ERROR -> false;
    };

Each case must produce a value, and every possible input value must be covered. The following two switch expressions do not compile. The Java compiler fails with the error "the switch expression does not cover all possible input values".

    // does not compile
    enum Status {
      SUBMITTED, PROCESSING, READY, ERROR
    }

    Status currentStatus = Status.SUBMITTED;
    boolean ok = switch(currentStatus) {
      case READY -> true;
      case ERROR -> false;
    };
   // does not compile
    String input = "TWO";
    int number = switch (input) {
      case "ONE" -> 1;
      case "TWO" -> 2;
      case "THREE" -> 3;
    };

To fix that you must add a default case or add a case for every possible value, which is only possible with enums. Both of these switch expressions compile because they cover every possible input value.

    boolean ok = switch(currentStatus) {
      case READY -> true;
      case ERROR -> false;
      default -> false;
    };
   boolean ok = switch(currentStatus) {
      case SUBMITTED, PROCESSING, READY -> true;
      case ERROR -> false;
    };

Arrow syntax, statement

You can also use the arrow style switch as a statement. Like in the expression form, the new switch statement no longer falls through. But there are some changes compared to the expression form. No longer need the switch to cover every possible value, and the cases no longer produce a value.

The following code prints 1

    String input = "ONE";
    switch (input) {
      case "ONE" -> System.out.println(1);
      case "TWO" -> {
        System.out.println("result");
        System.out.println(2);
      }
      case "THREE" -> System.out.println(3);
     }

If multiple input values have to be handled the same way list them comma separated after the case keyword.

    String input = "1";
    switch (input) {
      case "1", "2", "3" -> System.out.println("less than 4");
      case "4" -> System.out.println("four");
    }

Classic syntax, expression

Because the developers of the Java language wanted orthogonal support, they also added an expression version to the class switch.

When you write code with this syntax, you have to use the restricted identifier yield to produce values.

    String input = "TWO";
    int number = switch (input) {
      case "ONE": yield 1;
      case "TWO": yield 2;
      case "THREE": yield 3;
      default: yield -1;
    };

Multiple code lines in a case don't have to be wrapped in curly braces.

    String input = "ONE";
    int number = switch (input) {
      case "ONE":
        System.out.println("processing");
        int result = 2 - 1;
        yield result;
      case "TWO": yield 2;
      case "THREE": yield 3;
      default: yield -1;
    };

The switch keeps its fall-through behavior. The number variable in the following example is set to 3.

    String input = "TWO";
    int number = switch (input) {
      case "ONE":
      case "TWO":
      case "THREE": yield 3;
      default: yield -1;
    };

At least the last case must yield a value.

Like with the arrow syntax switch expression, every possible input value must be covered. Either by adding a default case or (only possible for enum) specifying a case for each possible value.

   enum Status {
      SUBMITTED, PROCESSING, READY, ERROR
    }

    Status currentStatus = Status.ERROR;
    boolean ok = switch(currentStatus) {
      case SUBMITTED:
      case PROCESSING:
      case READY: yield true;
      case ERROR: yield false;
    };

This concludes the overview of the three new switch constructs introduced in Java 14.
If you want to dive deeper, check out the specification JEP 361.


Preview

Time does not stand still, and we can already see and try the next change coming to switch. The current Java 19 release includes a preview feature called Pattern Matching for switch.

This feature is related to the Pattern Matching for instanceof feature that has been introduced in Java 16.

Before Java 16, we had to write code with the instanceof operator like this.

    Object obj = "Hello world";
    if (obj instanceof String) {
      String s = (String) obj;
      System.out.println(s.toUpperCase());
    }

The problem is the required cast, although we know that inside the if block obj is a String instance.

Starting with Java 16 you can simplify the code

    Object obj = "Hello world";
    if (obj instanceof String s) {
      System.out.println(s.toUpperCase());
    }

If obj is an instance of String, the code casts it to String, and the value is assigned to the variable s. The variable is only visible inside the if block.


The preview feature mentioned above in Java 17 and 18 allows us to write a similar construct with switch. The following code example is valid syntax in Java 18 (with enabled preview flag).

    record Point(int i, int j) {}
    enum Color { RED, GREEN, BLUE; }

    Object obj = new Point(1,1);

    switch (obj) {
      case null     -> System.out.println("null");
      case String s -> System.out.println("String");
      case Color c  -> System.out.println("Color with " + c.values().length + " values");
      case Point p  -> System.out.println("Record class: " + p.toString());
      case int[] ia -> System.out.println("Array of ints of length" + ia.length);
      default       -> System.out.println("Something else");
    }

The instanceof operator is implied here. Each case tests if obj is an instance of the specified type. If true, it casts and assigns the value to the variable, which is visible in the corresponding code on the right side of the case.

Note that this is currently a preview feature. It might change until the release.