W3docs

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.

FeatureTraditional switch (case L:)Modern switch (case L ->)
Fall-throughYes, unless you breakNo, each branch is isolated
Produces a valueNoYes (it is an expression)
Multiple labelsStacked empty case linesComma-separated on one line
Scope of variablesShared across the whole blockLocal 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();
    };
}
Note
Before pattern matching, a 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.

java— editable, runs on the server

What to take from the run:

  • kind() returns the result of a switch expression directly—MONDAY and FRIDAY print weekday, SATURDAY prints weekend, all without a single break.
  • The two arrow branches in kind() cover all seven enum constants, so the switch is exhaustive and needs no default.
  • 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 default block, which first prints (failing score 42) via the side-effect line and then yields 0—showing how a block branch can do work before producing its value.
  • The final switch assigns TWO straight into the label variable, 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 enum or the subtypes of a sealed type.

For the full history and the colon-label form, see Java switch expressions.

Practice

Practice
In a modern switch expression, which keyword produces a value from inside a block (the { ... } form of a branch)?
In a modern switch expression, which keyword produces a value from inside a block (the { ... } form of a branch)?
Was this page helpful?