Optional & Modern Java

Switch Expressions

15 min Lesson 8 of 13

Switch Expressions

Java has had the switch statement since version 1.0. For decades it worked, but it came with sharp edges: forgotten breaks caused silent fall-through bugs, you could not use switch where a value was required, and the scope rules for local variables were surprising. Java 14 made switch expressions a permanent feature, and the enhancements continued through Java 17 and 21. This lesson covers the modern form thoroughly.

The Problem With the Classic switch Statement

Consider a method that maps a day-of-week to a category:

// Old style — statement form, fall-through trap String classify(DayOfWeek day) { String result; switch (day) { case MONDAY: case TUESDAY: case WEDNESDAY: case THURSDAY: case FRIDAY: result = "Weekday"; break; // <-- forget this and you get "Weekend" for FRIDAY case SATURDAY: case SUNDAY: result = "Weekend"; break; default: throw new IllegalArgumentException("Unknown day: " + day); } return result; }

Three problems are visible here: (1) the repetitive case X: labels, (2) the mandatory break to prevent fall-through, and (3) the need for a mutable result variable. Switch expressions fix all three.

Arrow-Case Syntax: Switch as an Expression

The arrow form uses -> instead of :. Each arm is a standalone rule — there is no fall-through, and no break is needed.

// Modern style — expression form, no fall-through String classify(DayOfWeek day) { return switch (day) { case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "Weekday"; case SATURDAY, SUNDAY -> "Weekend"; }; }

Notice several things at once:

  • Multiple labels on one case arm are separated by commas — no stacking of identical case X: lines.
  • The whole switch produces a value, assigned directly by return.
  • No default is needed here because DayOfWeek is an enum and all seven values are covered. The compiler verifies this at compile time — exhaustiveness is enforced.
Exhaustiveness is a compile-time guarantee. If you cover an enum in a switch expression and later add a new constant to the enum, the switch expression will fail to compile, forcing you to handle the new case. This is the correct failure mode — it surfaces the gap at build time, not as a runtime bug at 2 a.m.

Returning Complex Values: the yield Keyword

The right-hand side of an arrow arm can be a single expression, but sometimes you need more logic — a local variable, an if, or a loop. In that case you use a block body and the yield keyword to produce the switch value:

int score(String grade) { return switch (grade) { case "A" -> 100; case "B" -> 85; case "C" -> 70; case "D" -> 55; default -> { System.err.println("Unrecognised grade: " + grade); yield 0; // yield supplies the value from a block arm } }; }

yield is not the same as return. return exits the enclosing method; yield exits only the switch block and hands its value to the switch expression. You cannot use return inside a switch block to supply the expression value.

Keep block arms rare. If every arm needs a block, consider moving the per-arm logic into a helper method and calling it from a one-liner arrow arm. Block arms add noise; reserve them for the occasional case that genuinely needs local state.

Mixing Arrow Arms With a Default

For non-enum selectors — String, int, or arbitrary objects in Java 21+ pattern switches — exhaustiveness cannot always be verified at compile time, so a default arm is required:

String httpMessage(int status) { return switch (status) { case 200 -> "OK"; case 301, 302 -> "Redirect"; case 400 -> "Bad Request"; case 401 -> "Unauthorised"; case 403 -> "Forbidden"; case 404 -> "Not Found"; case 500 -> "Internal Server Error"; default -> "Unknown (" + status + ")"; }; }
Do not mix arrow arms and colon arms in the same switch. A switch can use either the arrow form or the colon form throughout, but not both. Mixing them is a compile error. The colon form still supports fall-through and still requires yield (not break) to produce an expression value — another reason to prefer arrow arms exclusively in new code.

Assigning the Result to a Variable

A switch expression is just an expression. Anywhere a value is valid, a switch expression is valid — assignment, method argument, ternary operand, or return:

// assignment boolean isWeekend = switch (today) { case SATURDAY, SUNDAY -> true; default -> false; }; // inline in a method call System.out.println(switch (today) { case SATURDAY, SUNDAY -> "Rest day"; default -> "Work day"; });

Pattern Matching in Switch (Java 21)

Java 21 extended switch expressions with type patterns (preview in 17, finalised in 21). You can now match on the runtime type of an object:

String describe(Object obj) { return switch (obj) { case Integer i -> "Integer: " + i; case Double d -> "Double: " + d; case String s -> "String of length " + s.length(); case null -> "null"; default -> "Other: " + obj.getClass().getSimpleName(); }; }

The pattern variable (i, d, s) is scoped to that arm only and is already cast — no explicit cast required, no ClassCastException possible. The null case can now be handled inside the switch rather than with a pre-check, and exhaustiveness is still enforced when matching sealed hierarchies.

Trade-offs and When to Use Each Form

  • Use arrow-case switch expressions whenever you need to map a value to a result. This is the majority of switch use-cases.
  • Use yield only when an arm genuinely needs multi-line logic that cannot be extracted to a helper.
  • Avoid the colon form in new code. The only reason to keep it is when intentional fall-through is the clearest way to express a rule — which is rare.
  • Prefer switch expressions over long if-else chains on a single variable. The compiler can check exhaustiveness; an if-else chain cannot.

Summary

Switch expressions transform switch from a control-flow statement into a value-producing construct. The arrow syntax eliminates fall-through and removes the need for break. Multiple labels on a single arm replace stacked case lines. yield lets block arms produce a value without exiting the method. Exhaustiveness is enforced at compile time for enums and sealed types. Together these features make switch both safer and more expressive — a first-class tool for mapping and dispatching, not just a legacy control-flow mechanism.