Java Switch
A switch selects one branch by comparing a single value against a list of cases. Where an if/else if chain tests a series of arbitrary conditions, switch answers one question: which of these known values is it? Java has two forms: the traditional colon-and-break form inherited from C, and the arrow form introduced in Java 14, which can also produce a value.
Arrow Form (Java 14+)
int day = 3;
switch (day) {
case 1 -> System.out.println("Monday");
case 2 -> System.out.println("Tuesday");
case 3 -> System.out.println("Wednesday");
case 4 -> System.out.println("Thursday");
case 5 -> System.out.println("Friday");
case 6, 7 -> System.out.println("Weekend");
default -> System.out.println("Invalid day");
}
Output:
Wednesday
How it works:
- The expression in parentheses (
day) is evaluated once. - Its value is compared against each
caselabel. - The matching case's code runs, then the switch ends. No other case runs.
- If nothing matches,
defaultruns. Without adefault, nothing happens.
case 6, 7 lists several values that share one branch. A branch with more than one statement uses a block:
case 6, 7 -> {
System.out.println("Weekend");
planTrip();
}
Switch Expressions
The arrow form can produce a value, so a whole switch can sit on the right side of an assignment, in a return, or as a method argument.
int month = 2;
int year = 2028;
int days = switch (month) {
case 1, 3, 5, 7, 8, 10, 12 -> 31;
case 4, 6, 9, 11 -> 30;
case 2 -> (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 ? 29 : 28;
default -> throw new IllegalArgumentException("Invalid month: " + month);
};
System.out.println(days); // 29
A switch expression must be exhaustive: it has to produce a value for every possible input. For an int or String that means a default is required. A branch may also throw instead of producing a value, as the default does here.
The semicolon after the closing brace belongs to the assignment statement, not the switch.
yield
When a branch in a switch expression needs several statements, it uses a block and yield to supply the value:
String level = switch (score / 10) {
case 10, 9 -> "Excellent";
case 8, 7 -> "Good";
case 6 -> {
log("Borderline score: " + score);
yield "Average";
}
default -> "Needs improvement";
};
yield is to a switch expression what return is to a method: it ends the branch and provides the result. return cannot be used for this, because it would leave the enclosing method.
Traditional Form (Colon and break)
Older code, and code that must compile on Java 13 or earlier, uses colons:
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;
default:
System.out.println("Other day");
}
Fall-through
In the colon form, a matching case runs its statements and then continues into every following case until it reaches a break or the end of the switch. This is called fall-through.
int day = 2;
switch (day) {
case 1:
System.out.println("Monday");
case 2:
System.out.println("Tuesday");
case 3:
System.out.println("Wednesday");
default:
System.out.println("Other day");
}
Output:
Tuesday
Wednesday
Other day
The match at case 2 starts execution, and without any break it runs straight to the end. Forgetting a break is the classic switch bug, and it is why the arrow form was added: arrow cases never fall through.
Fall-through is occasionally intentional, to group labels:
switch (ch) {
case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
vowels++;
break;
default:
consonants++;
}
In the arrow form the same grouping is written case 'a', 'e', 'i', 'o', 'u' -> vowels++;.
The two forms cannot be mixed in a single switch: every case uses -> or every case uses :.
Which Types Can Be Switched On
| Type | Since |
|---|---|
byte, short, char, int and their wrappers | Java 1.0 / 5 |
enum | Java 5 |
String | Java 7 |
| Any reference type, using type patterns | Java 21 |
long, float, double, and boolean cannot be switched on with constant labels. Use if/else for those.
Case labels must be compile-time constants (literals, final constants, or enum constants), and each may appear only once.
Switching on strings
String command = "stop";
switch (command) {
case "start" -> startEngine();
case "stop" -> stopEngine();
case "status" -> printStatus();
default -> System.out.println("Unknown command: " + command);
}
String matching uses equals, so it is case-sensitive: "Stop" matches only the default. Normalize first with command.toLowerCase() if case should not matter. Switching on a null string throws NullPointerException unless a case null label is present (Java 21+).
Switching on enums
enum Status { PENDING, SHIPPED, DELIVERED, CANCELLED }
Status s = Status.SHIPPED;
String message = switch (s) {
case PENDING -> "Preparing your order";
case SHIPPED -> "On the way";
case DELIVERED -> "Delivered";
case CANCELLED -> "Order cancelled";
};
Inside the switch, enum constants are written without the type prefix (SHIPPED, not Status.SHIPPED). This switch expression has no default and still compiles, because it covers every constant of the enum, so the compiler knows it is exhaustive. If a new constant such as RETURNED is later added, this switch stops compiling until the new case is handled. Omitting default on enum switch expressions turns that change into a compile error rather than a silent gap.
Pattern Matching in switch (Java 21+)
A switch can test the type of an object and bind a variable:
static String describe(Object obj) {
return switch (obj) {
case null -> "null";
case Integer i when i < 0 -> "negative integer " + i;
case Integer i -> "integer " + i;
case String s when s.isBlank() -> "blank string";
case String s -> "string of length " + s.length();
case int[] arr -> "int array of " + arr.length;
default -> "something else: " + obj.getClass().getSimpleName();
};
}
describe(-5); // negative integer -5
describe(42); // integer 42
describe(" "); // blank string
describe("hi"); // string of length 2
describe(3.5); // something else: Double
case Integer i matches when obj is an Integer and binds it to i, already cast. A when clause (a guard) adds a condition that must also hold. Cases are checked top to bottom, so the more specific guarded case must come before the general one; the compiler rejects a case that an earlier case fully covers.
Combined with sealed interfaces and records, this allows exhaustive switches over a closed set of types, covered in the records and sealed classes lessons.
switch Versus if/else
| Situation | Better choice |
|---|---|
| One variable compared against several fixed values | switch |
Ranges (score >= 90) | if/else if |
| Conditions involving different variables | if/else if |
| Choosing a value from a known set | switch expression |
| Branching on the type of an object | pattern switch |
long, double, boolean values | if/else |
A switch makes the "one value, several known options" structure visible, and with enums and sealed types the compiler can verify that every option is handled.
Practical Example: Command Menu
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
public class TodoMenu {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
List<String> tasks = new ArrayList<>();
boolean running = true;
while (running) {
System.out.print("\n[a]dd [l]ist [d]elete [q]uit > ");
String choice = sc.nextLine().strip().toLowerCase();
switch (choice) {
case "a", "add" -> {
System.out.print("Task: ");
tasks.add(sc.nextLine().strip());
}
case "l", "list" -> {
if (tasks.isEmpty()) {
System.out.println("No tasks.");
}
for (int i = 0; i < tasks.size(); i++) {
System.out.println((i + 1) + ". " + tasks.get(i));
}
}
case "d", "delete" -> {
System.out.print("Number to delete: ");
int n = Integer.parseInt(sc.nextLine().strip());
if (n >= 1 && n <= tasks.size()) {
System.out.println("Removed: " + tasks.remove(n - 1));
} else {
System.out.println("No such task.");
}
}
case "q", "quit" -> running = false;
default -> System.out.println("Unknown option: " + choice);
}
}
}
}
The input is normalized with strip().toLowerCase() so that "A" and " add " both match. Each case accepts a short and a long command. Multi-statement branches use blocks, and because the arrow form never falls through, no break statements are needed. Setting running = false ends the surrounding while loop. The delete branch would throw on non-numeric input; production code would validate it as shown in the User Input lesson.