skip to content

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.

day =
old switch statementtype = "almost"
  1. 1switch (day) {
  2. 2 case "sat":
  3. 3 case "sun":
  4. 4 type = "weekend";
  5. 5 case "fri":
  6. 6 type = "almost";
  7. 7 break;
  8. 8 default:
  9. 9 type = "weekday";
  10. 10}
arrow switch expressiontype = "weekend"
  1. 1String type = switch (day) {
  2. 2 case "sat", "sun" -> "weekend";
  3. 3 case "fri" -> "almost";
  4. 4 default -> "weekday";
  5. 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.

pick a day. on the left, the lines that run are amber, and the ones that only run because of the missing 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

Main.java
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);
    }
}
output
sat weekend
fri almost
mon weekday

three upgrades in one:

  • no fallthrough. each -> arm runs on its own. no break needed, ever.
  • several labels per case: case "sat", "sun" ->.
  • it's an expression. the whole switch has 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:

Main.java
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);
}
output
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.

as a =
cases =
Main.java
  1. 1enum Size { S, M, L }
  2. 2
  3. 3String label = switch (size) {
  4. 4 case S -> "small";
  5. 5 case M -> "medium";
  6. 6};
$ javac Main.java
error: the switch expression does not cover all possible input values

an expression has to produce a value for every Size. L would have none, so javac refuses.

toggle the cases and switch between an expression and a statement. the error is the one javac 25 prints. the old-style statement compiles no matter what you leave out: unmatched values just fall out the bottom.

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.)

predict the output
java
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

typesince
int, short, byte, char (and their wrappers)always
enumsjava 5
Stringjava 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, booleanpreview only, still in 27
Main.java
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);
}
output
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.

one level deeper: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.

hover a stage. what javac 25 emits for switch (day) { case "sat" ... case "sun" ... }, read off javap -c.

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.

keylines
  • old colon cases are labels: execution falls through until a break.
  • arrow case X -> runs one arm. no fallthrough, no break.
  • a switch expression produces a value. use yield to 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+).