Branching on a single value against a list of constants is tedious with if / else if. You end up repeating the variable name, cluttering the logic, and inviting off-by-one errors in the comparisons. Java's switch exists for exactly this situation: it matches one value against a set of case labels and jumps straight to the match.
The classic switch has been in Java since the beginning, but it carries a sharp edge: fall-through. When a matching case has no break, execution keeps rolling into the next case, and the next, until something stops it. That can be deliberate — stacking several cases in front of one shared block — but more often it is a bug that ships to production.
The classic switch statement
A switch statement compares the value in parentheses against each case label for equality. When a match is found, execution starts at that label and continues downward. A break statement exits the switch entirely; without one, the program falls through into the next case and keeps running. A default label catches anything that did not match an explicit case.
The values in case labels must be compile-time constants: literal numbers, characters, or enum constants. You cannot use a variable as a case label, even if that variable never changes. This restriction lets the compiler build an efficient jump table, but it also means switch is only for fixed, known-ahead-of-time values.
flowchart TD SW["switch (day)"] --> C1["case 1:"] C1 --> P1["print(Mon)"] P1 --> B1["break"] B1 --> E["exit switch"] SW --> C2["case 2:"] C2 --> P2["print(Tue)"] P2 --> C3["case 3:"] C3 --> P3["print(Wed)"] P3 --> B3["break"] B3 --> E style P2 fill:#b45309,color:#fff style C3 fill:#b45309,color:#fff
public class Main {
public static void main(String[] args) {
int day = 2;
switch (day) {
case 1:
System.out.println("Monday");
break;
case 2:
case 3:
System.out.println("Midweek");
break;
case 6:
case 7:
System.out.println("Weekend");
break;
default:
System.out.println("Other");
}
}
}
Switch expressions: -> and yield
Modern Java (14+) adds a safer form: the switch expression. Instead of printing inside each case, the switch returns a value that you assign to a variable. The arrow syntax -> means exactly one result per case, and it never falls through. There is no need for break, because each arrow case is self-contained.
If a case needs multiple statements before producing its value, use the yield keyword instead of an arrow. The switch expression must be exhaustive: every possible value must map to a case, or the compiler rejects it. A default clause satisfies that requirement when the value is not an enum.
Switch expressions remove an entire category of bug. If you have ever debugged a missing break at 2 a.m., you already know why this matters.
flowchart LR
subgraph ST["Switch statement"]
S1["case 1:"] --> P1["print(A)"]
P1 --> B1["break needed"]
end
subgraph EX["Switch expression"]
S2["case 1 -> "A""] --> V1["returns value"]
S3["case 2 -> "B""] --> V2["returns value"]
end
style ST fill:#b45309,color:#fff
style EX fill:#166534,color:#fff
public class Main {
public static void main(String[] args) {
int score = 85;
String grade = switch (score / 10) {
case 10, 9 -> "A";
case 8 -> "B";
case 7 -> "C";
case 6 -> "D";
default -> {
System.out.println("Below threshold");
yield "F";
}
};
System.out.println("Grade: " + grade);
}
}
Enums: named constants with type safety
An enum is a type with a fixed set of named values. Define it once, and the compiler treats it as a real type — not just a number with a label. Day.MONDAY is a Day; it is not an int and cannot be confused with one.
Enums shine inside switches. Because the compiler knows every possible value, it can check that your switch is exhaustive: every enum constant must have a case, or a default, or the code will not compile. That turns a whole class of runtime mistakes into build-time errors. Enum switches also read more clearly than integer switches: case MONDAY says what it means in plain English.
flowchart TD
E["enum Day { MON, TUE, WED }"] --> S["switch (d)"]
S --> C1["case MON -> ..."]
S --> C2["case TUE -> ..."]
S --> C3["case WED -> ..."]
C3 --> OK["Compiler verifies:<br/>all cases covered"]
style E fill:#c2410c,color:#fff
style OK fill:#166534,color:#fff
Classic switch with a missing break. Predict the exact output (watch the fall-through).
public class Main {
public static void main(String[] args) {
int level = 2;
switch (level) {
case 1:
System.out.println("Low");
case 2:
System.out.println("Medium");
case 3:
System.out.println("High");
break;
default:
System.out.println("Unknown");
}
}
}
level is 2, so execution starts at case 2.
case 2 has no break, so it falls through into case 3.
case 3 prints High and then breaks.
In a switch expression using arrow syntax (->), what happens if you omit a break?
Switch expressions with -> are self-contained: each arrow produces one value and there is no fall-through. You do not write break, and you cannot accidentally fall through. That is exactly why they are safer than classic switches.
Switch expression with yield. Predict the exact output (two lines).
public class Main {
public static void main(String[] args) {
int n = 4;
String result = switch (n) {
case 1, 2, 3 -> "small";
case 4, 5 -> "medium";
default -> {
System.out.println("big");
yield "large";
}
};
System.out.println(result);
}
}
n is 4, which matches case 4, 5 -> medium.
The default block does not run because 4 is covered.
The switch expression returns medium, which is then printed.
Why is a switch over an enum safer than a switch over an int?
Because an enum has a fixed, known set of values, the compiler can verify exhaustiveness. An int switch could receive any integer, so the compiler cannot know whether your cases cover every possibility.
Enum switch puzzle. Predict the exact output (two lines) — work through the enum and the switch expression carefully.
public class Main {
enum Status { PENDING, ACTIVE, CLOSED }
public static void main(String[] args) {
Status s = Status.ACTIVE;
String label = switch (s) {
case PENDING -> "wait";
case ACTIVE -> "go";
case CLOSED -> "done";
};
System.out.println(label);
System.out.println(s == Status.ACTIVE);
}
}
s is Status.ACTIVE, so the switch expression returns go.
s == Status.ACTIVE compares enum references. Since s was assigned ACTIVE, this is true.
No default is needed because the switch covers all three enum constants.
Recap
- A classic switch matches a value against
caselabels. Withoutbreak, execution falls through into the next case. - A switch expression with
->returns a value and never falls through. Useyieldinside a block to return a value. - Switch expressions must be exhaustive; the compiler rejects incomplete ones.
- An enum defines a closed set of named constants. Switching over an enum lets the compiler verify that every constant is covered.
- Prefer switch expressions over classic switches when you are computing a value; they remove an entire category of fall-through bugs.
Next you will learn loop patterns and invariants — the mental models that make loops predictable instead of mysterious.