Topic 2.4
Switch Expressions and Arrow Labels
In one line
Since Java 14, switch can be an expression that produces a value, and case X -> arrow labels never fall through. The compiler checks that a switch expression covers every possible input, and Java 21 adds pattern matching, guards and case null on top.
Think of it like this
A ticket machine at a museum. You press 'child', 'adult' or 'senior' and it hands you back exactly one price. It never gives you two tickets by accident, and if someone added a new button without a price, the machine wouldn't be allowed to ship. A switch expression is that machine: one input, exactly one answer, every option covered.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Switch expression
- A
switchthat produces a value you can assign or return, likeint x = switch (...) { ... };. - Arrow label
- A
case X ->label. Only the code to its right runs; it never falls through. - yield
- A statement inside a switch-expression block that says 'this is the switch's value'. It's a contextual keyword, so it's only special inside a switch expression.
- Exhaustive
- Covering every possible input. The compiler refuses a switch expression that might find no matching case.
- Pattern
- A shape to test a value against, like 'is an Integer'. If it matches, the value is also given a name you can use.
- Guard
- An extra
whencondition on a pattern case, likecase Integer i when i > 0. - Sealed type
- A class or interface that lists every class allowed to extend it, so the compiler knows all the possibilities (Java 17, Phase 5).
Step by step
01Arrow labels: no fall-through, no break
Replace case 1: with case 1 ->. The arrow form runs exactly the one expression, block or throw on its right, and then leaves the switch. You can't forget a break, because there's nothing to forget.
Group values with commas. This is a switch statement (it prints, it doesn't produce a value), written with arrows.
int day = 6;
switch (day) {
case 1, 2, 3, 4, 5 -> System.out.println("Weekday");
case 6, 7 -> System.out.println("Weekend");
default -> System.out.println("Not a day");
}02A switch that returns a value
Put the switch on the right of =. Each arm's expression becomes the value. The variable gets assigned in one place, it can be final, and the compiler checks every path gives a value, so no 'might not have been initialized' errors.
Compare this with Topic 2.3: no breaks, no declared-but-unassigned variable, no fall-through risk.
int day = 3;
String name = switch (day) {
case 1 -> "Monday";
case 2 -> "Tuesday";
case 3 -> "Wednesday";
default -> "Unknown";
}; // <- semicolon: this is one assignment statement
System.out.println(name); // Wednesday03Blocks and yield
When an arm needs more than one expression, give it a block in braces. Inside, yield hands back the value. Think of yield as return, but for the switch instead of the method.
Each block must end by yielding a value or throwing. If control can reach the closing brace without either, javac says switch rule completes without providing a value.
int score = 72;
String band = switch (score / 10) {
case 10, 9 -> "A";
case 8 -> "B";
case 7 -> {
String base = "C";
yield score % 10 >= 5 ? base + "+" : base; // 72 -> "C"
}
default -> "below C";
};04Exhaustiveness: the compiler checks every case
A switch expression must produce a value for every input. With an int selector there are four billion possible values, so you need default. With an enum, covering every constant is enough.
This is the main reason to prefer switch expressions for enums. Add THU to the enum later, and every switch expression that doesn't handle it fails to compile, pointing you to each place that needs updating. A classic switch statement would silently do nothing for THU.
enum Day { MON, TUE, WED }
Day d = Day.TUE;
String type = switch (d) {
case MON -> "start";
case TUE -> "middle";
// WED missing -> compile error
};05Colon labels and yield, and the one rule about mixing
A switch expression may also use the old colon labels with yield. Fall-through comes back with them: below, code = 2 prints two, falls into case 3, and yields 30.
You must pick one style per switch. Mixing case 1: and case 2 -> in the same switch is error: different case kinds used in the switch. In practice, use arrows for new code.
int code = 2;
int x = switch (code) {
case 1: yield 10;
case 2: System.out.println("two"); // falls through
case 3: yield 30;
default: yield 0;
};
System.out.println(x); // two, then 3006Java 21: patterns, guards and case null
From Java 21 the selector can be any reference type, and a case can test the value's type and name it at the same time: case Integer i -> means 'if it's an Integer, call it i'. A when guard adds a condition.
case null handles null explicitly instead of throwing. Cases are checked top to bottom, and the compiler rejects a case that can never match because an earlier, broader case already covers it (a dominated label).
static String describe(Object o) {
return switch (o) {
case null -> "nothing";
case Integer i when i > 100 -> "big number " + i;
case Integer i -> "number " + i;
case String s -> "text of length " + s.length();
default -> "something else";
};
}07What the compiler adds for you
When you cover every enum constant without default, the compiler still adds a hidden default arm. It only runs if the enum changed after your code was compiled (a new constant in a library jar). It throws IncompatibleClassChangeError for code compiled for Java 14–20 and MatchException for code compiled for Java 21 and later, rather than silently returning nothing.
Switch expressions on int, String and enums compile to the same tableswitch/lookupswitch bytecode as Topic 2.3. Pattern switches (Java 21) compile to an invokedynamic call to java.lang.runtime.SwitchBootstraps.typeSwitch, which works out the matching case index at run time.
Try it yourself
- 1
Break exhaustiveness on purpose
In 'Exhaustive enum switch', add
JUPITERto the enum. Compile and read the error. Then addcase JUPITER -> "giant";and run again. - 2
Turn a statement into an expression
Take the first practice answer from Topic 2.3 (days in a month) and rewrite
daysInasreturn switch (month) { ... };with arrow labels. Count how many lines disappear. - 3
Reorder pattern cases
In 'Pattern matching for switch' (needs Java 21), move
case Integer i ->abovecase Integer i when i > 100 ->. Compile. The errorthis case label is dominated by a preceding case labeltells you the guarded case could never run.
Code & diagrams
Expected output
1: weekday
6: weekend
9: not a dayExpected output
100 -> A
84 -> B
77 -> C+
72 -> C
40 -> below C
Size M costs 3Add `JUPITER` to the enum and this no longer compiles until you handle it. That is the point.
Expected output
MERCURY: inner, hot
VENUS: inner, hot
EARTH: home
MARS: redExpected output
nothing
big number 500
number 7
empty text
text of length 5
something else: DoubleBoth compute the same value. The new form can't fall through, assigns once, and is checked for exhaustiveness.
// Classic statement (Topic 2.3)
String name;
switch (day) {
case 1: name = "Mon"; break;
case 2: name = "Tue"; break;
default: name = "?";
}
// Switch expression (Java 14+)
String name2 = switch (day) {
case 1 -> "Mon";
case 2 -> "Tue";
default -> "?";
};Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Leave out a case in a switch expression
Write a switch expression on an int with case 1 and case 2 but no default.
public class Main {
public static void main(String[] args) {
int d = 9;
String n = switch (d) {
case 1 -> "Mon";
case 2 -> "Tue";
};
System.out.println(n);
}
}Break #2
Forget yield in a block
Give an arm a block that computes a value but never yields it.
public class Main {
public static void main(String[] args) {
int d = 3;
String n = switch (d) {
case 1 -> "Mon";
default -> {
String t = "Day " + d;
}
};
System.out.println(n);
}
}Break #3
Mix colon and arrow labels
Write case 1: ... break; and case 2 -> ... in the same switch.
Myth vs fact
Myth
Arrow labels only work in switch expressions.
Fact
They work in switch statements too, and remove fall-through there as well.
Myth
yield is a reserved keyword everywhere.
Fact
It's a contextual keyword: special only as a statement inside a switch expression. (Since Java 14 you can't call a method named yield without qualifying it, though.)
Myth
An enum switch expression always needs a default.
Fact
Covering every constant makes it exhaustive. Leaving out default is better: the compiler then flags new constants.
Myth
Switch expressions are slower than the old switch.
Fact
For int, String and enum selectors they compile to the same tableswitch/lookupswitch bytecode.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Switch expressions arrived as a preview in Java 12 and 13 (JEP 325, JEP 354;
break valuein 12 becameyieldin 13) and became standard in Java 14 (JEP 361). Pattern matching for switch was previewed in 17–20 and finalised in Java 21 (JEP 441), together with record patterns (JEP 440). - ▸
Exhaustive switches without
defaultget a synthetic default that throws. JEP 441 changed it fromIncompatibleClassChangeErrortoMatchExceptionfor code compiled with the Java 21 rules. It's a separate-compilation safety net, not something your tests will normally hit. - ▸
Pattern switches compile to
invokedynamicbootstrapped bySwitchBootstraps.typeSwitch(orenumSwitch), which returns the index of the first matching label; guards are re-checked in a loop that restarts the search from the next index when a guard fails. - ▸
Exhaustiveness plus sealed interfaces gives you algebraic data types:
sealed interface Shape permits Circle, Squareand a switch overShapewith nodefaultfails to compile when a new shape is added. It is the modern Java replacement for much of the Visitor pattern (see the System Design course's design patterns).
Remember this
- 1
An arrow label
case 1 -> ...runs only the code on its right: one expression, one block in braces, or athrow. There is no fall-through and nobreakneeded. Several values share one arrow with commas:case SAT, SUN -> "weekend". Arrow labels work in switch statements too, not only expressions. - 2
A switch expression produces a value:
String name = switch (day) { case 1 -> "Mon"; ... };. Note the semicolon after the closing brace: the whole thing is part of an assignment statement. - 3
When an arm needs several statements, use a block and finish it with
yield value;.yieldmeans 'this is the value of the switch'.returncan't be used for this (it would leave the whole method), andbreakout of a switch expression is a compile error. - 4
A switch expression must be exhaustive: every possible selector value must be handled. For
intorStringthat means adefault. For anenum, listing every constant is enough, with nodefault. If someone later adds a constant, the code stops compiling instead of silently doing nothing, which is the biggest safety win of this feature. - 5
Colon labels still work inside a switch expression (
case 1: yield 10;) and still fall through, but you can't mix:and->labels in one switch (different case kinds used in the switch). - 6
Java 21 (JEP 441) adds pattern matching for switch: any reference type as a selector, type patterns (
case Integer i ->), guards (case Integer i when i > 100 ->),case null, and exhaustiveness checks over sealed hierarchies (Phase 5). You'll use these heavily with records later in the course.
Explain it without notes
What are the differences between case X: and case X ->?
Why can't you use return to give a switch expression its value, and what do you use instead?
What does exhaustive mean for a switch expression, and why is it better to list every enum constant than to add a default?
What did Java 21 add to switch?
Practice
Rewrite the month-length switch from Topic 2.3 as a switch expression with arrow labels, and print the lengths for months 2, 4 and 12.
Write a switch expression that maps a traffic light enum (RED, YELLOW, GREEN) to the seconds until change (30, 5, 25), with no default. Print all three.
Using a switch expression on a String command ("add", "sub", "mul") compute the result for a = 6 and b = 3; for anything else, yield 0 from a block that also prints unknown command. Test with "mul" and "pow".
Trade-offs
- ↔
Switch expressions need Java 14+; older codebases (Java 8 and 11 are still common) can only use the classic statement.
- ↔
Leaving out
defaulton an enum switch gives compile-time safety when constants are added, but a library that adds constants can then break callers that compiled against the old version (they getMatchExceptionat run time). - ↔
Pattern switches replace long
instanceofchains neatly, but overusing them on open class hierarchies scatters behaviour; for types you own and extend often, polymorphism (Phase 5) may fit better.
Done when you can
Done when you can write a switch expression with arrow labels, a block arm and
yield.Done when you can explain why a switch expression must be exhaustive and fix the error when it isn't.
Done when you choose to omit
defaulton enum switch expressions and can say why.Done when you can read a Java 21 pattern switch with guards and
case null.Done when you can convert any classic switch statement to the arrow form without changing its behaviour.