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
| Form | Use it |
|---|---|
| expression with arrows | whenever you are producing a value — the default choice |
| statement with arrows | when the branches perform actions rather than produce a value |
| statement with colons | never 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.