Java Switch
The switch statement is Java’s way of choosing between many possible paths of execution based on the value of a single variable, without writing a long chain of if / else if comparisons. It reads more clearly than a stack of equality checks, and in many cases the compiler and JVM can execute it far more efficiently than an equivalent chain of if statements. Since Java 14, switch can also be used as an expression that directly produces a value, which is now the preferred style for new code. This lesson covers both the classic statement form and the modern expression form, how each is executed internally, and the mistakes that trip up almost every Java learner at least once.
Overview: How the Switch Statement Works
A switch evaluates one expression once, then compares that value against a list of constant case labels. When a match is found, execution jumps directly to that label and continues running statements from there — including falling into the next case if you don’t stop it, which is the single most important thing to understand about classic switch. If no label matches, the optional default block runs instead; if there’s no default and nothing matches, the switch simply does nothing and control moves to the statement after it.
switch can operate on: byte, short, char, int (and their wrapper classes Byte, Short, Character, Integer), String (since Java 7), and enum types. It cannot operate on long, float, double, or boolean — those either can’t be represented as compile-time constant labels efficiently or are better handled with if. Every case label must itself be a compile-time constant (a literal, a final constant, or an enum constant) — you cannot use a variable or a range like case x > 5.
There are two forms in modern Java. The classic (statement) form uses case value: labels, falls through by default, and requires an explicit break to stop. The arrow (expression) form, introduced in Java 14, uses case value ->, never falls through, and can directly return a value that you assign to a variable. Both compile to similar bytecode; the arrow form is simply safer and terser syntax layered on top.
Syntax
The classic statement form looks like this:
switch (expression) {
case value1:
// statements
break;
case value2:
case value3:
// statements for value2 or value3
break;
default:
// statements if nothing matched
}
| Part | Meaning |
|---|---|
expression |
The value being tested; must be byte/short/char/int, their wrappers, String, or an enum. |
case valueN: |
A compile-time constant to compare against. Stacking labels (as with value2 and value3 above) groups them to share one block. |
break; |
Exits the switch immediately. Without it, execution falls through into the next case’s statements. |
default: |
Runs when no case matches. Optional, but strongly recommended. Can appear anywhere, though convention places it last. |
The modern expression form (Java 14+) looks like this:
type result = switch (expression) {
case value1 -> singleExpressionOrValue;
case value2, value3 -> {
// multiple statements
yield someValue;
}
default -> fallbackValue;
};
Here, -> means “no fallthrough, run only this” and yield is how a multi-statement block hands back its result (a single expression after -> doesn’t need yield).
Examples
Example 1: Basic switch on an int
public class Main {
public static void main(String[] args) {
int day = 3;
switch (day) {
case 1:
System.out.println("Monday");
break;
case 2:
System.out.println("Tuesday");
break;
case 3:
System.out.println("Wednesday");
break;
case 4:
System.out.println("Thursday");
break;
case 5:
System.out.println("Friday");
break;
case 6:
case 7:
System.out.println("Weekend");
break;
default:
System.out.println("Invalid day");
}
}
}
Output:
Wednesday
Java evaluates day once, finds the label case 3, jumps straight to System.out.println("Wednesday"), then hits break and exits. Cases 6 and 7 are stacked together deliberately, so either value runs the same “Weekend” line — this is the safe, intentional use of fallthrough.
Example 2: Grouped cases for a real calculation
public class Main {
public static void main(String[] args) {
int month = 4;
int year = 2024;
int days;
switch (month) {
case 1: case 3: case 5: case 7: case 8: case 10: case 12:
days = 31;
break;
case 4: case 6: case 9: case 11:
days = 30;
break;
case 2:
days = (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)) ? 29 : 28;
break;
default:
days = 0;
}
System.out.println("Month " + month + " has " + days + " days");
}
}
Output:
Month 4 has 30 days
This is where switch shines over a long if / else if chain: seven case labels can share one statement without repeating code, and the logic reads like a table. Month 4 matches the second group, so days becomes 30.
Example 3: Modern switch expression with String and yield
public class Main {
public static void main(String[] args) {
String day = "WED";
int dayNumber = switch (day) {
case "MON" -> 1;
case "TUE" -> 2;
case "WED" -> 3;
case "THU" -> 4;
case "FRI" -> 5;
case "SAT", "SUN" -> {
System.out.println("Weekend!");
yield 0;
}
default -> throw new IllegalArgumentException("Unknown day: " + day);
};
System.out.println("Day number: " + dayNumber);
}
}
Output:
Day number: 3
Here switch is used as an expression: it produces a value that is assigned directly to dayNumber. Each arrow case is self-contained with no fallthrough risk. The "SAT", "SUN" case shows a multi-statement block using yield to return its value, and the default shows a switch expression can throw instead of returning, which is useful for rejecting invalid input.
Under the Hood: How the JVM Executes a Switch
The bytecode the compiler generates depends on how densely packed the case values are. When the int case values are close together (like 1, 2, 3, 4, 5), javac emits a tableswitch instruction: a literal jump table indexed directly by the value, giving O(1) lookup regardless of how many cases exist. When the values are sparse (like 10, 200, 5000), it emits a lookupswitch, which does a binary search over sorted case values — still much faster than a linear chain of if comparisons for large numbers of cases.
Switching on a String is actually syntactic sugar: the compiler rewrites it into a switch on the string’s hashCode() (an int, so it becomes a tableswitch/lookupswitch), followed by an equals() check inside each matched branch to guard against hash collisions. Switching on an enum is also sugar: the compiler generates a hidden synthetic array mapping each enum constant’s ordinal() to a case index, then switches on that int. This is also why case labels for an enum switch use the bare constant name (case MONDAY:), not Day.MONDAY.
The modern arrow-form switch expression compiles down to the same underlying tableswitch/lookupswitch instructions — it’s a language-level improvement for safety and expressiveness, not a different runtime mechanism.
Common Mistakes
Mistake 1: Forgetting break causes accidental fallthrough
char grade = 'A';
switch (grade) {
case 'A':
System.out.println("Excellent");
case 'B':
System.out.println("Good");
break;
default:
System.out.println("Unknown");
}
Output:
Excellent
Good
Because case 'A' has no break, execution falls straight into case 'B'‘s statements even though grade is 'A', not 'B'. This is the classic switch bug: the fix is to add break after every case block that shouldn’t flow into the next one.
char grade = 'A';
switch (grade) {
case 'A':
System.out.println("Excellent");
break;
case 'B':
System.out.println("Good");
break;
default:
System.out.println("Unknown");
}
Output:
Excellent
With the break restored, only the matching case runs. If you’re on Java 14+, switching to the arrow form (case 'A' -> ...) removes this entire class of bug, since arrow cases never fall through.
Mistake 2: Switching on a null reference throws NullPointerException
String status = null;
try {
switch (status) {
case "ACTIVE":
System.out.println("Active");
break;
default:
System.out.println("Other");
}
} catch (NullPointerException e) {
System.out.println("Caught NPE: switch cannot evaluate a null selector");
}
Output:
Caught NPE: switch cannot evaluate a null selector
A classic switch must be able to evaluate its selector expression to compare it against case labels; when that selector is null (a String, a boxed wrapper like Integer, or an enum reference), the JVM throws NullPointerException before checking any case. Always validate or default a nullable value before switching on it, e.g. if (status == null) status = "UNKNOWN";.
Mistake 3: Missing a default case
Skipping default is legal, but it means unexpected input is silently ignored — no case runs and no error is raised. This hides bugs when new enum constants or unexpected values appear later. Always include a default, even if it just logs or throws, so unhandled cases are visible rather than silent.
Mistake 4: Trying to switch on unsupported types
Code such as switch (someDouble) or switch (isReady) where isReady is a boolean will not compile — switch only accepts byte, short, char, int, their wrapper types, String, and enum values. For a boolean, use a plain if / else; for floating-point comparisons, exact equality is unreliable anyway, so a switch would be the wrong tool regardless.
Best Practices
- Prefer the arrow form (
->) over the colon form in new code written for Java 14 or later — it eliminates accidental fallthrough entirely. - Always include a
defaultcase, even just to throw an exception for unexpected values, so bugs surface immediately instead of being silently ignored. - When intentionally grouping cases with fallthrough in the classic form, add a short comment noting it’s deliberate, since a reader can’t otherwise tell it apart from a missing
break. - Guard against
nullbefore switching on aString, boxed wrapper, or nullable enum reference. - Use
switchinstead of a longif / else ifchain when comparing one variable against many discrete constant values — it’s clearer to read and the JVM can execute it faster via jump tables. - For enums, let the compiler help you: many IDEs and static analyzers warn when a switch on an enum is missing a case for a newly added constant.
Practice Exercises
- Write a program that reads an integer month number (1–12) from a
Scannerand uses a classicswitchstatement to print the season (“Winter”, “Spring”, “Summer”, “Fall”), grouping the appropriate months together with stacked case labels. - Rewrite the season program above using a Java 14+ switch expression that assigns the season name directly to a variable, instead of printing inside each branch.
- Write a program with a
charvariable holding a letter grade ('A'through'F'). Using a switch expression, assign a description (“Excellent”, “Good”, “Average”, “Poor”, “Fail”) to aStringvariable, and throw anIllegalArgumentExceptionin thedefaultcase for any other character.
Summary
switchcompares one expression against a list of compile-time constant case labels and jumps to the first match.- It works on
byte,short,char,intand their wrappers,String, andenum— notlong,float,double, orboolean. - The classic colon form falls through to the next case unless you add
break; the modern arrow form (Java 14+) never falls through. - A switch expression can directly produce and return a value using
->oryield, instead of just running statements. - Internally, dense int switches compile to a
tableswitchjump table, sparse ones to alookupswitchbinary search,Stringswitches to hashCode + equals checks, andenumswitches to an ordinal-based lookup array. - Switching on a
nullreference throwsNullPointerException; always guard nullable values first. - Always include a
defaultcase so unexpected values are handled explicitly rather than silently ignored.
