Java 17 Switch Expressions

July 24, 20265 min readUpdated 8/20/2026

The switch statement Java inherited from C had two design flaws: it fell through unless you wrote break, and it could not produce a value. Java 14 fixed both, and Java 17 made it standard. The old form still works; there is no reason to write it in new code.

Arrow labels

class Demo {
    void oldForm(int day) {
        switch (day) {
            case 1:
                System.out.println("Monday");
                break;              // forget this and Tuesday prints too
            case 2:
                System.out.println("Tuesday");
                break;
            default:
                System.out.println("Other");
        }
    }

    void newForm(int day) {
        switch (day) {
            case 1 -> System.out.println("Monday");
            case 2 -> System.out.println("Tuesday");
            default -> System.out.println("Other");
        }
    }
}

With -> only the matching branch runs. There is no fall-through, so there is no break to forget — an entire category of bug removed by punctuation.

A branch needing more than one statement takes a block:

class Demo {
    void run(int day) {
        switch (day) {
            case 6, 7 -> {                          // several labels, comma-separated
                System.out.println("weekend");
                System.out.println("no alarm");
            }
            default -> System.out.println("weekday");
        }
    }
}

Switch as an expression

The larger change: a switch can now produce a value. This is what removes the assign-a-variable-in-every-branch pattern:

class Demo {
    String oldWay(int day) {
        String type;                                 // declared, then assigned four times
        switch (day) {
            case 1: case 2: case 3: case 4: case 5:
                type = "weekday";
                break;
            case 6: case 7:
                type = "weekend";
                break;
            default:
                type = "invalid";
        }
        return type;
    }

    String newWay(int day) {
        return switch (day) {                        // one expression, one assignment
            case 1, 2, 3, 4, 5 -> "weekday";
            case 6, 7 -> "weekend";
            default -> "invalid";
        };                                           // note the semicolon
    }
}

The semicolon after the closing brace is easy to miss and the compiler error is clear when you do. It is there because this is an expression inside a statement, like any other assignment.

yield

When a branch of a switch expression needs several statements, it uses a block — and a block has to say which value it produces. That is what yield is for:

class Demo {
    int daysInMonth(int month, int year) {
        return switch (month) {
            case 1, 3, 5, 7, 8, 10, 12 -> 31;
            case 4, 6, 9, 11 -> 30;
            case 2 -> {
                boolean leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
                yield leap ? 29 : 28;                // not `return` — that would exit the method
            }
            default -> throw new IllegalArgumentException("bad month: " + month);
        };
    }
}

yield exits the switch with a value; return exits the whole method. Using return inside a switch expression does not compile, which is the language stopping a genuinely confusing construct.

Note also the default -> throw. A branch may throw instead of producing a value, and that is often the right answer for an input that should be impossible.

Exhaustiveness

A switch expression must cover every possible input, or it does not compile. A switch statement need not. That difference is the source of the feature's biggest practical benefit:

enum Status { PENDING, SHIPPED, DELIVERED }

class Demo {
    String describe(Status status) {
        return switch (status) {          // no default — all three covered
            case PENDING -> "waiting";
            case SHIPPED -> "on its way";
            case DELIVERED -> "arrived";
        };
    }
}

Add a CANCELLED constant to that enum and this method stops compiling, along with every other exhaustive switch over Status in the codebase. The compiler hands you the list of places to update.

That is why you should resist adding a default branch to an enum switch. A default makes it compile forever, and the new constant silently falls into it — turning a compile error into a runtime surprise. If you genuinely need a fallback, throwing is better than returning something plausible:

enum Status { PENDING, SHIPPED, DELIVERED }

class Demo {
    String risky(Status status) {
        return switch (status) {
            case PENDING -> "waiting";
            default -> "something else";   // a new constant lands here, silently
        };
    }
}

Strings, and null

class Demo {
    int parse(String command) {
        return switch (command) {
            case "start", "run" -> 1;
            case "stop" -> 0;
            default -> -1;
        };
    }
}

Switching on strings has worked since Java 7 and uses equals, not ==, so it behaves as you would want. It is also case-sensitive, which is worth remembering when the input comes from a user.

A null selector throws NullPointerException — including in the arrow form, and including when there is a default. The default branch does not catch null. Guard before the switch, or use case null, which Java 21 added.

Which form to use

FormUse it
expression with arrowswhenever you are producing a value — the default choice
statement with arrowswhen the branches perform actions rather than produce a value
statement with colonsnever in new code; recognise it in old code

The one legitimate use of the colon form is deliberate fall-through, and it is rare enough that when you see it you should check whether it was deliberate at all.

A note on scope

One small improvement the arrow form brings that nobody mentions: each branch gets its own scope. In the colon form, all the cases share one, so a variable declared in the first case is visible — and uninitialised — in the others:

class Demo {
    void colonForm(int day) {
        switch (day) {
            case 1:
                String label = "Monday";
                System.out.println(label);
                break;
            case 2:
                // `label` is in scope HERE too, and definitely unassigned.
                // Declaring another `String label` would not compile.
                System.out.println("Tuesday");
        }
    }

    void arrowForm(int day) {
        switch (day) {
            case 1 -> {
                String label = "Monday";        // scoped to this block only
                System.out.println(label);
            }
            case 2 -> {
                String label = "Tuesday";       // a second declaration is fine
                System.out.println(label);
            }
        }
    }
}

Next

Text Blocks is next — multi-line strings without the escaping.