switch
the old fallthrough switch vs the new arrow switch expression.
- chapter
- chapter 02.07
- reading time
- 6 min read
switch picks one branch by matching a value against cases. the old colon form falls through from one case into the next unless you break. the arrow form (case X ->) runs exactly one arm, and as a switch expression it produces a value, and the compiler checks you've covered every input. prefer arrows.
the picture
an if / else if chain asks a question at every step. a switch is more like a train station departure board: look up the value once, go straight to the matching platform.
java has two kinds of switch that look almost the same and behave very differently. the old one is from 1996 and copies C, fallthrough and all. the new one arrived in java 14 (JEP 361) and fixes it.
fallthrough, the old trap
in the colon form, case "sat": is just a label: a place to jump to. once execution lands there, it runs every line below it, straight through the next case labels, until it hits a break or the end of the switch.
- 1switch (day) {
- 2 case "sat":
- 3 case "sun":
- 4 type = "weekend";
- 5 case "fri":
- 6 type = "almost";
- 7 break;
- 8 default:
- 9 type = "weekday";
- 10}
- 1String type = switch (day) {
- 2 case "sat", "sun" -> "weekend";
- 3 case "fri" -> "almost";
- 4 default -> "weekday";
- 5};
jumps to case "sat", falls through the empty case "sun" (on purpose), sets weekend, then keeps falling into case "fri" (not on purpose) and overwrites it with almost. the arrow version can't do this: each arm runs alone.
break are red. on the right, the arrow version runs one arm and stops. both outputs were checked on jdk 25.fallthrough is occasionally what you want (that's how case "sat": and case "sun": share one body), but forgetting a break is far more common than wanting one. it's been a top bug in C-family languages for fifty years.
the arrow form
void main() {
for (String day : new String[] {"sat", "fri", "mon"}) {
String type = switch (day) {
case "sat", "sun" -> "weekend";
case "fri" -> "almost";
default -> "weekday";
};
IO.println(day + " " + type);
}
}sat weekend fri almost mon weekday
three upgrades in one:
- no fallthrough. each
->arm runs on its own. nobreakneeded, ever. - several labels per case:
case "sat", "sun" ->. - it's an expression. the whole
switchhas a value you can assign, return or pass, instead of setting a variable in every branch.
when an arm needs more than one line, give it a block and say which value it produces with yield:
void main() {
int code = 404;
String msg = switch (code) {
case 200 -> "ok";
case 404 -> {
String path = "/missing";
yield "not found: " + path;
}
default -> "status " + code;
};
IO.println(msg);
}not found: /missing
(yield instead of return, because return would leave the whole method.)
covering every case
a switch expression has to produce a value for every possible input, so the compiler checks. for an enum, listing every constant counts as complete, no default needed.
- 1enum Size { S, M, L }
- 2
- 3String label = switch (size) {
- 4 case S -> "small";
- 5 case M -> "medium";
- 6};
an expression has to produce a value for every Size. L would have none, so javac refuses.
this is the quiet superpower of switch expressions: add XL to the enum next year, and every switch expression that forgot it becomes a compile error instead of a silent bug. (that's also why it's often better to leave out default on enums: a default would catch XL and hide it.)
String day = "sun";
String type = "?";
switch (day) {
case "sat":
case "sun":
type = "weekend";
case "fri":
type = "almost";
break;
default:
type = "weekday";
}
IO.println(type);what you can switch on
| type | since |
|---|---|
int, short, byte, char (and their wrappers) | always |
| enums | java 5 |
String | java 7 (compared with equals, after a hashCode check) |
any object, with type patterns: case Integer i when i > 40 -> | java 21 (pattern matching) |
long, float, double, boolean | preview only, still in 27 |
void main() {
Object o = 42;
String d = switch (o) {
case Integer i when i > 40 -> "big int " + i;
case Integer i -> "int " + i;
case String s -> "string of length " + s.length();
default -> "something else";
};
IO.println(d);
}big int 42
that last form is a different beast: it checks the type of the value, binds it to a variable, and can add a when condition. it gets its own chapter in pattern matching.
how a switch compiles+
for int cases, javac emits one of two bytecode instructions. when the cases are dense (1, 2, 3), it uses tableswitch: an array of jump targets indexed by the value, one bounds check and one jump, no matter how many cases. when they're sparse (1, 1000, 1000000), it uses lookupswitch: a sorted list of keys the JVM can binary-search. you can see both with javap -c. a String switch compiles to a switch on hashCode() first, then an equals check to rule out collisions, then a second switch on the result:
hover a stage.
all of which is why a switch can be faster than a long if / else if chain, though the JIT often makes the difference disappear.
- old colon
cases are labels: execution falls through until abreak. - arrow
case X ->runs one arm. no fallthrough, no break. - a switch expression produces a value. use
yieldto return one from a block. - switch expressions must be exhaustive, and the compiler checks. enums don't need a default.
- switch on a null throws, unless there's a
case null(java 21+).