Modern Java Switch Expressions
Use modern switch expressions in Java with arrow labels, yield, exhaustiveness, and pattern matching.
The traditional switch statement has been part of Java since version 1.0, but it carried a lot of baggage: fall-through bugs, repetitive break statements, and no way to produce a value. Switch expressions, finalized in Java 14, fix all of that. They turn switch from a clumsy control-flow statement into a concise, value-producing expression.
This chapter walks through the modern switch: arrow labels, the yield keyword, multi-label cases, exhaustiveness checking, and exactly how the new form differs from the statement you may already know.
From Statement to Expression
The classic switch statement runs side effects and relies on break to stop fall-through. Forget a break and execution silently tumbles into the next case—a notorious source of bugs.
// Traditional switch statement (error-prone)
String kind;
switch (day) {
case SATURDAY:
case SUNDAY:
kind = "weekend";
break; // forget this and you fall through
default:
kind = "weekday";
}A switch expression collapses that into a single assignment. The arrow (->) form never falls through, so no break is needed.
// Modern switch expression
String kind = switch (day) {
case SATURDAY, SUNDAY -> "weekend";
default -> "weekday";
};The whole switch now evaluates to a value, which you can assign, return, or pass as an argument directly.
Arrow Labels and Multi-Label Cases
The arrow label case L -> associates one label (or several, comma-separated) with a single action. Only the matching branch runs—there is no fall-through to worry about.
int numLetters = switch (month) {
case JANUARY, JUNE, JULY -> 4;
case FEBRUARY, MARCH, APRIL, MAY -> 5;
case SEPTEMBER, OCTOBER, NOVEMBER, DECEMBER -> switchOnLength(month);
case AUGUST -> 6;
};Grouping labels with commas replaces the old trick of stacking empty case lines to share a body, and it reads far more clearly.
| Feature | Traditional switch (case L:) | Modern switch (case L ->) |
|---|---|---|
| Fall-through | Yes, unless you break | No, each branch is isolated |
| Produces a value | No | Yes (it is an expression) |
| Multiple labels | Stacked empty case lines | Comma-separated on one line |
| Scope of variables | Shared across the whole block | Local to each branch block |
Blocks and the yield Keyword
When a branch needs more than a single expression, use a block { ... } and return its value with yield. The yield keyword is to a switch expression what return is to a method: it supplies the value the branch produces.
int gradePoints = switch (grade) {
case 'A' -> 4;
case 'B' -> 3;
default -> {
log("Unknown grade: " + grade);
yield 0; // the value this branch evaluates to
}
};You can still use colon labels with yield if you prefer the old syntax, but the arrow form is the idiomatic modern choice and avoids accidental fall-through entirely.
Exhaustiveness and default
A switch expression must be exhaustive: every possible input must be handled, because the expression has to produce a value no matter what. For most types you satisfy this with a default branch. For an enum, the compiler can verify exhaustiveness directly—if you cover every constant, default becomes optional.
// No default needed: all enum constants are covered,
// so the compiler knows the switch is exhaustive.
boolean isWeekend = switch (day) {
case SATURDAY, SUNDAY -> true;
case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> false;
};If you omit a constant and there is no default, the code will not compile. This compile-time safety net is one of the biggest practical wins over the old statement, which would silently leave a variable unassigned.
Pattern Matching in switch
Recent Java versions extend switch so each case can match on the type of the value, not just a constant. This is pattern matching for switch (a preview in Java 17–20, finalized in Java 21). Instead of writing a chain of instanceof checks and casts, you label each branch with a type pattern and bind a variable in the same step.
static String describe(Object obj) {
return switch (obj) {
case Integer i -> "int " + i; // matches and binds i
case String s -> "string of length " + s.length();
case null -> "nothing"; // null can be its own label
default -> "something else";
};
}Pattern matching is especially powerful with records and sealed types: when a switch covers every permitted subtype of a sealed type, the compiler treats it as exhaustive, so you can drop default entirely.
sealed interface Shape permits Circle, Square {}
record Circle(double radius) implements Shape {}
record Square(double side) implements Shape {}
static double area(Shape shape) {
return switch (shape) { // no default: all permitted types covered
case Circle c -> Math.PI * c.radius() * c.radius();
case Square s -> s.side() * s.side();
};
}switch could only test an enum, an integral type, or a String. Type patterns let switch work on any reference type, which makes it a natural replacement for long if/else if ladders.A Complete Worked Example
The program below puts the pieces together: arrow labels with multi-label cases, a yield block for a computed branch, exhaustive enum coverage, and a switch expression assigned straight to a variable.
What to take from the run:
kind()returns the result of a switch expression directly—MONDAYandFRIDAYprintweekday,SATURDAYprintsweekend, all without a singlebreak.- The two arrow branches in
kind()cover all seven enum constants, so the switch is exhaustive and needs nodefault. - In
letterGrade(), scores 95, 83, 71, and 64 map cleanly through arrow labels to 4, 3, 2, and 1 grade points. - The score 42 hits the
defaultblock, which first prints(failing score 42)via the side-effect line and thenyields 0—showing how a block branch can do work before producing its value. - The final
switchassignsTWOstraight into thelabelvariable, proving a switch expression is a value you can store, not just control flow.
When to Use Which Form
- Reach for a switch expression (
case L ->) whenever the goal is to compute and return a single value. It is exhaustive, fall-through-free, and reads as one assignment. - A traditional switch statement still makes sense when each branch is purely a side effect (logging, dispatching) and there is no value to produce.
- Use type patterns when you are branching on the runtime type of an object, particularly across the constants of an
enumor the subtypes of a sealed type.
For the full history and the colon-label form, see Java switch expressions.