Topics › Basics › Things to Know in C++/Java/Python or any language
Switch Case
Java source ↗ Edit this article ↗
Jump to code:
What this covers
A switch compares one value against a list of constants and jumps to the matching branch. It replaces a long if / else if chain of the form x == 1, x == 2, x == 3 with something both shorter and, for the compiler, often faster. Java’s switch has grown over the years: the classic statement with case labels and break, the arrow form that removes fall-through, the switch expression that produces a value, and in Java 21 pattern matching that switches on the runtime type of an object. This article shows each form, when to prefer it, and the fall-through trap that explains why the newer forms exist.
The selector can be an int (or byte, short, char), a String, an enum, or, with pattern matching, any object. It cannot be a long, a double or a boolean. Case labels must be compile-time constants, and no two may be equal.
The classic switch statement
The original syntax lists case value: labels followed by statements. Several labels can be stacked in front of one body, which is how “months with 31 days” becomes a single branch.
static int daysInMonth(int month, int year) {
boolean leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
switch (month) {
case 1: case 3: case 5: case 7: case 8: case 10: case 12:
return 31;
case 4: case 6: case 9: case 11:
return 30;
case 2:
return leap ? 29 : 28;
default:
return -1;
}
}
default is the branch for “none of the above”; here it reports an invalid month. Each branch ends with return, which leaves the method and therefore the switch. If a branch does not return, it must end with break, or execution falls through into the next branch.
Fall-through, and why it is usually a bug
After a case label matches, Java executes statements downward until it meets break, return or the end of the switch; intervening case labels are ignored. Forgetting a break therefore runs two branches. Occasionally that is what you want: an “unlock everything up to this level” rule reads naturally as cascading cases.
static String unlockedLevels(int level) {
StringBuilder sb = new StringBuilder();
switch (level) {
case 3: sb.append("hard ");
case 2: sb.append("medium ");
case 1: sb.append("easy");
break;
default: sb.append("none");
}
return sb.toString();
}
unlockedLevels(3) returns "hard medium easy": case 3 matched, and with no break in sight execution continued through cases 2 and 1 until the break. unlockedLevels(2) gives "medium easy". The default sits last and is only reached directly, since the break in case 1 stops the cascade. When you rely on fall-through, add a comment; when you do not, prefer the arrow form, which makes it impossible.
The arrow form and switch expressions
Since Java 14, case label -> body replaces the colon. There is no fall-through: exactly one arm runs. Multiple labels are separated by commas, and the whole switch can be an expression whose value is whatever the chosen arm produces.
static String dayType(String day) {
return switch (day) {
case "SAT", "SUN" -> "weekend";
case "MON", "TUE", "WED", "THU", "FRI" -> "weekday";
default -> "unknown";
};
}
A switch expression must be exhaustive: for a String selector that means a default arm is mandatory, because the compiler cannot know every possible string. Switching on a String compares with equals, so it is case-sensitive ("sat" falls to default) and a null selector throws NullPointerException. Note the semicolon after the closing brace: the switch is an expression inside a return statement.
Characters work the same way, and the compact arms make lookup tables readable:
static int romanValue(char c) {
return switch (c) {
case 'I' -> 1;
case 'V' -> 5;
case 'X' -> 10;
case 'L' -> 50;
case 'C' -> 100;
case 'D' -> 500;
case 'M' -> 1000;
default -> 0;
};
}
Blocks and yield
When an arm needs more than one statement, wrap them in braces. Inside such a block the value is handed back with yield, because return would try to leave the whole method. An arm may also throw, which counts as producing no value and keeps the expression exhaustive.
static int calculate(int a, char op, int b) {
return switch (op) {
case '+' -> a + b;
case '-' -> a - b;
case '*' -> a * b;
case '/' -> {
if (b == 0) yield 0;
yield a / b;
}
case '%' -> b == 0 ? 0 : a % b;
default -> throw new IllegalArgumentException("unknown operator " + op);
};
}
The / arm guards against division by zero before dividing, and the % arm does the same with a ternary. An unknown operator throws, and the test in main confirms that with a try/catch. A thrown exception is often the right behaviour for a default that “cannot happen”: a silent 0 would hide a bug.
Pattern matching on types (Java 21)
The newest form switches on the type of an object. Each arm names a type and a variable that is bound to the selector when the type matches, so there is no cast. A when clause adds a guard, and case null handles a null selector explicitly instead of throwing.
static String describe(Object o) {
return switch (o) {
case null -> "null";
case Integer i when i < 0 -> "negative int " + i;
case Integer i -> "int " + i;
case String s -> "string of length " + s.length();
default -> "something else";
};
}
Arms are tested top to bottom, and the compiler rejects an arm that can never match because an earlier one dominates it: putting case Integer i before case Integer i when i < 0 is a compile error. describe(5) autoboxes to Integer and yields "int 5", describe(-3) takes the guarded arm, and describe(2.5) is a Double and lands in default. For sealed interfaces and records the compiler can even verify exhaustiveness without a default, which turns a forgotten case into a compile error rather than a runtime surprise.
Choosing between if and switch
Use switch when one value is compared against several constants: menu options, characters, small integer codes, day names. Use if / else if when conditions are ranges (marks >= 90), involve several variables, or are arbitrary boolean expressions, none of which a switch label can express. When a switch would need default to do real work for most inputs, an if is probably clearer.
Edge cases and pitfalls
- Missing
breakin classic switches. The most common switch bug. Prefer the arrow form, where it cannot happen. nullselector. A classic or arrow switch on anullStringorIntegerthrowsNullPointerException. Only pattern-matching switches withcase nullaccept it.- Case sensitivity on strings.
"sat"and"SAT"are different labels. Normalise withtoUpperCase()before switching if the input may vary. - Duplicate or non-constant labels. Labels must be distinct compile-time constants; a variable cannot be a label.
- Exhaustiveness. A switch expression over
int,charorStringneedsdefault;enumand sealed types can omit it when every value is covered. returnversusyield. Inside a block arm of a switch expression, useyieldto produce the arm’s value.- Dominance order. In pattern switches, guarded and more specific arms must come before general ones.
Related problems
- If ElseIf is the general-purpose branching construct that
switchspecialises. - Functions (Pass by Reference and Value) explains how the
calculatemethod receives its operands. - Cpp Basics covers the
chararithmetic behind switching on characters.