Home | Send Feedback | Share on Bluesky |

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 was 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 semantics. It's a statement, which 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. You 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 classic 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.


Pattern Matching for switch (JEP 441)

Java 21 introduced significant enhancements to the switch construct, allowing it to be used for more than just simple equality checks. This evolution is part of JEP 441 (Pattern Matching for switch), which enables powerful pattern matching capabilities within switch expressions.

JEP 441 introduces several powerful features:

Expanded Selector Expression Types and Basic Type Patterns

The switch selector can now be any reference type, not just primitives, String, or enums. This means you can switch directly on Object, interfaces, records, and arrays.

You can now use case Type var -> to match by type. If the selector matches Type, it's automatically cast and assigned to var, which is then in scope for the case block. This eliminates explicit instanceof checks and casts.

static String describeObject(Object obj) {
  return switch (obj) {
    case String s    -> "A String of length " + s.length();
    case Integer i   -> "An Integer: " + i;
    case Double d    -> "A Double: " + d;
    default          -> "Some other object type";
  };
}

Handling null Explicitly

Previously, null inputs to switch would throw a NullPointerException. Java 21 introduces case null -> to handle null directly within the switch block, treating it as another value.

static String handleValue(Object value) {
  return switch (value) {
    case null        -> "Value is null";
    case String s    -> "Value is a String: " + s;
    default          -> "Value is of unknown type";
  };
}

Guarded Patterns (when clause)

Add an optional when clause (a boolean expression) after a pattern label for more precise conditional logic. The pattern variable is in scope for the when expression.

static String analyzeInput(Object input) {
  return switch (input) {
    case String s when s.length() > 5 -> "Long String";
    case String s                     -> "Short String";
    case Integer i when i > 100       -> "Large Integer";
    default                           -> "Other type";
  };
}

Record Patterns

Deconstruct record values directly within case labels. Components are automatically extracted into individual pattern variables, simplifying data access.

record Point(int x, int y) {}

static String describePoint(Object obj) {
  return switch (obj) {
    case Point(int x, int y) -> "Deconstructed Point: X=" + x + ", Y=" + y;
    default                  -> "Not a Point";
  };
}

Nested Record Patterns

Deconstruct complex, hierarchical data structures by nesting record patterns within case labels, allowing direct access to deeply nested data.

record Name(String first, String last) {}
record Person(Name name, int age) {}

static String describePerson(Object obj) {
  return switch (obj) {
    case Person(Name(var firstName, var lastName), var age) ->
         "Person: " + firstName + " " + lastName + ", Age: " + age;
    default -> "Not a Person";
  };
}

Improved Enum Constant Handling

You can now use the full qualified name of an enum constant (e.g., EnumClass.CONSTANT) as a case label, especially useful in sealed hierarchies.

sealed interface CardClassification permits Suit {}
public enum Suit implements CardClassification { CLUBS, DIAMONDS }

static String classifyCard(CardClassification card) {
  return switch (card) {
    case Suit.CLUBS    -> "It's a Club";
    case Suit.DIAMONDS -> "It's a Diamond";
  };
}

Exhaustiveness and Sealed Types

switch expressions are required to be exhaustive. With sealed classes or interfaces, the compiler uses the permits clause to ensure all permitted subtypes are covered, potentially removing the need for a default clause and catching unhandled cases at compile time.

sealed interface Shape permits Circle, Rectangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double length, double width) implements Shape {}

static String describeShape(Shape shape) {
  return switch (shape) {
    case Circle c    -> "It's a circle";
    case Rectangle r -> "It's a rectangle";
    // No default needed if all sealed types are covered
  };
}

Dominance and Ordering

The compiler enforces dominance rules, meaning a case label cannot be unreachable due to a preceding, more general pattern. More specific patterns should generally appear before more general ones.

The following code does not compile because the general rule Integer i precedes the more specific rule Integer i when i == 42

static void testNumber(Number num) {
  switch (num) {
    case Integer i              -> System.out.println("Any Integer");
    case Integer i when i == 42 -> System.out.println("The Answer");
    case Double d               -> System.out.println("A Double");
    default                     -> System.out.println("Other Number");
  }
}

To fix it we have to switch the rules.

static void testNumber(Number num) {
  switch (num) {
    case Integer i when i == 42 -> System.out.println("The Answer");
    case Integer i              -> System.out.println("Any Integer");
    case Double d               -> System.out.println("A Double");
    default                     -> System.out.println("Other Number");
  }
}

Unnamed Variables & Patterns (JEP 456)

Java 22 introduced JEP 456 (Unnamed Variables & Patterns) a feature designed to reduce "visual noise" and improve code readability, especially in pattern matching scenarios. It allows you to use the underscore (_) as a placeholder for variables or pattern components that are syntactically required but not semantically used in your code.

This is particularly useful in switch statements when deconstructing records, where you might only care about a few components, but the pattern requires you to list all of them.

record Delay(int timeInMs, String type) {}
record Reverb(String name, int roomSize, double decay) {}
record EffectLoop(Delay delay, Reverb reverb) {}

static String analyzeEffectBetter(EffectLoop effectLoop) {
  return switch (effectLoop) {
    // Using '_' for 'type', 'name', and 'decay' as they are not used
    case EffectLoop(Delay(int timeInMs, _), Reverb(_, int roomSize, _)) -> {
      if (timeInMs == roomSize) {
        yield "Delay time equals reverb room size.";
      } else {
        yield "Delay time differs from reverb room size.";
      }
    }
    default -> "Unknown effect loop.";
  };
}

This simple change helps reduce boilerplate and makes your switch statements more focused and easier to understand, especially when dealing with complex data structures.


Preview: Primitive Types in Patterns, instanceof, and switch

Java continues to evolve its pattern matching capabilities. This preview feature in Java 24 and 25 JEP 507 aims to unify how primitive types (like int, double, boolean) are handled in pattern matching, making them work as seamlessly as reference types.

Primitive Type Patterns

You can now use primitive types directly in instanceof and switch patterns. The switch construct can now operate directly on boolean, long, int, float, and double values, in addition to existing types.

boolean isLoggedIn = true;
String status = switch (isLoggedIn) {
  case true -> "Logged In";
  case false -> "Guest";
};
long largeNumber = 10_000_000_000L;
switch (largeNumber) {
  case 1L -> System.out.println("One");
  case 10_000_000_000L -> System.out.println("Ten Billion");
  case long x -> System.out.println("Other long: " + x); // Catches any other long
}

Safe Primitive Conversions in Record Patterns

This allows deconstructing record components into primitive types, even if it's a narrowing conversion, as long as no information is lost. For example, a double component can be deconstructed into an int if its value is an exact integer.

record JsonNumber(double value) {}
Object obj = new JsonNumber(5.0);

switch (obj) {
  case JsonNumber(int i) when i < 10 -> System.out.println("JSON number as int: " + i);
  case JsonNumber(double d) -> System.out.println("JSON number as double: " + d);
  default -> System.out.println("Unknown type");
}

Note that this is preview feature and may change before it becomes a standard feature in a future Java release.