Switch Expressions
Reviewed & published by Brayan K
A switch expression is a modern form of switch that produces a value you can assign or return, uses arrow labels with no fall-through, and is checked by the compiler for exhaustiveness.
Part of the free Java course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
Before You Start
You should know the classic switch statement and enums. Switch expressions are standard from Java 14 onward.
What You'll Learn
- Writing a switch that returns a value
- Arrow labels and why they never fall through
- Using yield from a multi-statement block arm
- Grouping cases with comma-separated labels
- Exhaustiveness and when default is optional
- Migrating an old switch statement to an expression
The Big Idea — A Vending Machine, Not a To-Do List
💡 Analogy: The old switch statement is a to-do list: "if it's Monday, do these chores; remember to stop (break) or you'll keep doing the next day's chores too." The new switch expression is a vending machine: you put in a value, press the matching button (an arrow arm), and it hands you back exactly one product. No risk of accidentally dispensing every snack below the one you wanted.
That single returned value is the whole point: String role = switch (rank) { ... }; reads as one clear assignment instead of a mutable variable poked at by many case branches.
1️⃣ A switch That Returns a Value
Put the whole switch on the right of an assignment. Each arm uses case LABEL -> value;. The arrow makes each arm self-contained — there is no break, and no fall-through. Note the whole expression ends with a semicolon, just like any other assignment.
public class Main {
public static void main(String[] args) {
for (int day = 1; day <= 7; day++) {
// switch as an EXPRESSION: it produces a value with arrow labels.
String kind = switch (day) {
case 1, 2, 3, 4, 5 -> "weekday";
case 6, 7 -> "weekend";
default -> "invalid";
};
System.out.println("Day " + day + ": " + kind);
}
}
}2️⃣ Block Arms, yield, and Exhaustive Enums
When an arm needs more than one statement, wrap it in braces and end with yield to supply the value — yield is the switch-expression equivalent of return. And when you switch over an enum and cover every constant, the switch is exhaustive, so you can drop default entirely.
public class Main {
enum Coin { PENNY, NICKEL, DIME, QUARTER }
public static void main(String[] args) {
for (Coin c : Coin.values()) {
// A block arm uses { } and 'yield' to return its value.
int cents = switch (c) {
case PENNY -> 1;
case NICKEL -> 5;
case DIME -> 10;
case QUARTER -> {
int base = 25;
yield base; // 'yield' is the value of the block
}
};
System.out.println(c + " = " + cents + " cents");
}
// No 'default' needed: the enum cases are exhaustive.
}
}3️⃣ Old Statement vs New Expression
Side by side, the difference is stark. The statement form needs a mutable label variable and a break after each case (forget one and you get a fall-through bug). The expression form assigns a value directly and can never fall through.
public class Main {
public static void main(String[] args) {
// OLD statement switch: easy to forget 'break' -> fall-through bugs.
for (int n = 1; n <= 3; n++) {
String label;
switch (n) {
case 1:
label = "one";
break;
case 2:
label = "two";
break;
default:
label = "many";
}
System.out.println("statement: " + n + " -> " + label);
}
// NEW expression switch: arrows never fall through, and it's a value.
for (int n = 1; n <= 3; n++) {
String label = switch (n) {
case 1 -> "one";
case 2 -> "two";
default -> "many";
};
System.out.println("expression: " + n + " -> " + label);
}
}
}4️⃣ Multiple Labels & default
Group values that behave the same with a comma-separated label: case "GET", "HEAD" -> 200;. For open-ended types like String or int you still need a default arm so the switch stays exhaustive.
public class Main {
public static void main(String[] args) {
String[] inputs = {"GET", "POST", "PUT", "DELETE", "TRACE"};
for (String method : inputs) {
// Multiple labels share an arm; a default keeps it exhaustive.
int code = switch (method) {
case "GET", "HEAD" -> 200;
case "POST", "PUT" -> 201;
case "DELETE" -> 204;
default -> 405; // method not allowed
};
System.out.println(method + " -> " + code);
}
}
}🧠 Quick Recall
Predict the result before opening each answer.
int x = switch (n) {
case 1 -> 10;
case 2 -> 20;
};Answer: No — for an int input it is not exhaustive (what about 3, 4, ...?). error: the switch expression does not cover all possible input values. Add a default.
case 5 -> {
int v = 5 * 5;
break v;
}Answer: A block arm must return its value with yield v;, not break v;. break doesn't produce a value in a switch expression.
String s = switch (day) {
case TUE -> "two";
case WED -> "three";
default -> "?";
};Answer: No. Arrow arms never fall through — TUE yields "two" and stops. Only the classic colon form (without break) falls through.
🎯 Your Turn — Days in a month
Use a switch expression with a multi-label case to map a month number to its day count.
public class Main {
public static void main(String[] args) {
// YOUR TURN: use a switch EXPRESSION to map a month number (1-12) to
// its day count (ignore leap years). Group the 30-day months with a
// multi-label case, give February 28, and let default handle 31.
// Expected (first lines):
// Month 1: 31 days
// Month 2: 28 days
// Month 4: 30 days
for (int month = 1; month <= 12; month++) {
int days = 0; // replace 0 with: switch (month) { ... }
System.out.println("Month " + month + ": " + days + " days");
}
}
}🧩 Mini-Challenge — Tag log levels
Cover every enum constant (so no default) and use a block arm with yield for one of them.
public class Main {
enum Level { DEBUG, INFO, WARN, ERROR }
public static void main(String[] args) {
// MINI-CHALLENGE: tag each log level. DEBUG and INFO share one arm
// ("[normal] X"); WARN uses a { } block with yield ("[careful] X");
// ERROR is "[ALERT] X". Because you cover all four enum constants,
// you need NO default.
//
// Expected output:
// [normal] DEBUG
// [normal] INFO
// [careful] WARN
// [ALERT] ERROR
for (Level lv : Level.values()) {
String tag = ""; // replace with: switch (lv) { ... }
System.out.println(tag);
}
}
}Common Errors
- ❌ Forgetting the trailing semicolon. A switch expression assignment ends with ;. Fix: var x = switch(...) {...};.
- ❌ Using break value; in a block arm. Fix: use yield value; — only yield produces a value.
- ❌ Non-exhaustive switch over int/String. error: does not cover all possible input values. Fix: add a default arm.
- ❌ Mixing arrow and colon styles in one switch. Not allowed. Fix: pick one form for the whole switch.
- ❌ Expecting fall-through with arrows. There is none. Fix: list shared values in one label, e.g. case A, B -> ....
- ❌ Adding default to an exhaustive enum switch unnecessarily. It silences the helpful error when a new constant is added. Fix: omit it so the compiler reminds you later.
Pro Tips
- 💡 Reach for switch expressions to replace if-else chains that compute one value — they're shorter and exhaustiveness-checked.
- 💡 Prefer enums as the switch subject. You get compile-time exhaustiveness for free and a warning when someone adds a constant.
- 💡 Keep arms tiny. If an arm grows large, extract a method and call it from the arm to keep the switch readable.
- 💡 Pair with assignment to final/var. The value is computed once and never reassigned — cleaner than a mutable accumulator.
📋 Quick Reference
| Concept | Syntax | Note |
|---|---|---|
| Expression form | var x = switch (k) {...}; | Produces a value |
| Arrow arm | case 1 -> "one"; | No break, no fall-through |
| Multiple labels | case 1, 2, 3 -> ... | Share one arm |
| Block arm | case X -> { ... yield v; } | yield gives the value |
| Default | default -> ...; | Catches everything else |
| Enum exhaustive | all constants covered | default optional |
| Colon form value | case 1: yield x; | yield, not break |
| Switch on String | switch (s) {...} | Needs default |
| As a return | return switch (k) {...}; | Common pattern |
| Statement form | switch (k) { case 1 -> act(); } | Arrows, no value |
| Since | Java 14 | JEP 361 |
📚 Resources / Keep Learning
- 📘 Oracle: Switch Expressions and Statements — the official language guide.
- 📗 dev.java: Branching with switch — a guided tutorial.
- 📙 JEP 361: Switch Expressions — the finalizing proposal.
🎉 Lesson Complete!
You can now write a switch that returns a value, use arrow arms that never fall through, supply values from block arms with yield, group cases with multiple labels, and rely on exhaustiveness so an enum switch needs no default.
Practice quiz
What is the key difference between a switch expression and a switch statement?
- Expressions can only use int
- Statements are faster
- A switch expression produces a value you can assign
- Expressions require break
Answer: A switch expression produces a value you can assign. A switch expression evaluates to a value, so you can write var x = switch(...) {...};.
What separator do arrow-style switch labels use?
- -> (arrow)
- : (colon)
- => (fat arrow)
- | (pipe)
Answer: -> (arrow). Arrow labels use ->; the older statement form uses a colon with break.
Do arrow-style switch arms fall through to the next case?
- Yes, like the old switch
- Only without break
- Only for default
- No — each arrow arm runs exactly its own code
Answer: No — each arrow arm runs exactly its own code. Arrow arms never fall through, eliminating the classic missing-break bug.
How do you return a value from a multi-statement { } arm?
- return
- yield value
- break value
- = value
Answer: yield value. Inside a block arm you use yield to supply the arm's value.
How do you match several values in one arm?
- Comma-separate the labels: case 1, 2, 3 ->
- Repeat the case keyword
- Use ||
- Use a range a..b
Answer: Comma-separate the labels: case 1, 2, 3 ->. List the constants separated by commas in a single label: case 1, 2, 3 -> ...
When is a default branch NOT required in a switch expression?
- Never required
- Only for int
- When switching over an enum and all constants are covered
- Only for String
Answer: When switching over an enum and all constants are covered. If every enum constant has an arm, the switch is exhaustive and default is optional.
In which Java version did switch expressions become standard?
- Java 8
- Java 14
- Java 12
- Java 17
Answer: Java 14. Switch expressions previewed in 12/13 and were finalized in Java 14 (JEP 361).
What happens if a switch expression is not exhaustive?
- It returns null
- It throws at runtime
- It returns 0
- It is a compile error
Answer: It is a compile error. A switch expression must cover every possible value; otherwise the compiler rejects it.
Can you still use colon-and-yield together in a switch expression?
- No
- Yes — case 1: yield x; is valid, just without arrows
- Only in Java 21
- Only with enums
Answer: Yes — case 1: yield x; is valid, just without arrows. The colon form of a switch expression uses yield instead of break to produce a value.
Why is the arrow switch considered safer than the classic one?
- It is faster
- It allows null keys
- No fall-through and the compiler enforces exhaustiveness
- It supports more types
Answer: No fall-through and the compiler enforces exhaustiveness. No accidental fall-through plus required exhaustiveness catches whole classes of bugs at compile time.
Continue this course
- Previous: Text Blocks
- Next: Pattern Matching for instanceof — Test-and-cast in one step with binding patterns — no more redundant casts
- Quick reference: Java cheat sheet
Frequently asked questions
What makes a switch an expression rather than a statement?
A switch expression evaluates to a single value that you can assign, return, or pass as an argument: var size = switch (s) { ... };. A switch statement, by contrast, just performs actions and produces no value. The expression form was finalized in Java 14. You can spot it because the whole switch sits on the right-hand side of an assignment (or as a return value) and ends with a semicolon.
What is the difference between -> arms and yield?
An arrow arm whose right side is a single expression simply uses that expression as the value: case 1 -> "one";. When an arm needs several statements, you wrap them in braces and use yield to hand back the result: case 2 -> { var t = compute(); yield t; }. yield is to a switch expression what return is to a method — it supplies the value. You only need yield for multi-statement block arms.
Why don't arrow switch arms fall through?
In the classic colon-style switch, control 'falls through' to the next case unless you write break, which caused countless bugs. Arrow-style arms run exactly the code to the right of the arrow and then the switch is done — there is no fall-through at all. If you genuinely want two values to behave identically, you list them in one label: case 1, 2 -> ...; rather than relying on fall-through.
When do I need a default branch?
A switch expression must be exhaustive: every possible input value must be handled, because the expression has to produce a value no matter what. For an int or String you almost always need a default to cover 'everything else'. But when you switch over an enum and provide an arm for every constant, the compiler knows all cases are covered, so default is optional (and adding one can even hide the helpful error you'd get if someone adds a new enum constant later).
Can I mix multiple labels and blocks?
Yes. You can combine comma-separated labels with either single-expression or block arms freely. For example: case MON, TUE, WED, THU, FRI -> "work"; and case SAT, SUN -> { log("weekend"); yield "rest"; }. Multiple labels keep related cases together without duplicating the body, and block arms let those grouped cases run several statements before yielding a value.