The traditional switch statement has been part of Java since version 1.0, and for decades it has been one of the most common sources of subtle bugs. The problem is not the concept — branching on a value is fundamental to programming — but the implementation. The classic switch inherited its design from C, and with it came fall-through semantics: if you forget a break statement, execution silently falls through to the next case. This single design choice has caused more production bugs than anyone wants to count.
Consider this classic example of a fall-through bug:
// Classic fall-through bug -- spot the problem
public String getDayType(String day) {
String type;
switch (day) {
case "MONDAY":
case "TUESDAY":
case "WEDNESDAY":
case "THURSDAY":
case "FRIDAY":
type = "Weekday";
// Missing break! Falls through to "Weekend"
case "SATURDAY":
case "SUNDAY":
type = "Weekend";
break;
default:
type = "Unknown";
}
return type; // Always returns "Weekend" for weekdays!
}
That missing break means every weekday falls through to the “Weekend” case. The code compiles without warnings. It runs without exceptions. It simply returns the wrong answer. These bugs are notoriously hard to spot in code review and even harder to catch in testing if your test cases happen to start with “SATURDAY.”
Beyond fall-through, the traditional switch has other pain points:
break statement, bloating the codeswitch to a variable directly, so you must declare the variable before the switch and assign it inside each caseSwitch expressions fix all of this. They were introduced as a preview feature in Java 12 (JEP 325), refined in Java 13 (JEP 354), and became a permanent feature in Java 14 (JEP 361). In Java 17 — the current LTS release — switch expressions are stable, battle-tested, and should be your default choice over the traditional switch statement.
What switch expressions bring to the table:
| Feature | Traditional Switch | Switch Expression |
|---|---|---|
| Fall-through | Default behavior (bug-prone) | No fall-through with arrow syntax |
| Returns a value | No — it is a statement | Yes — it is an expression |
| Exhaustiveness | Not enforced | Compiler-enforced |
| Multiple labels | Stacked cases with fall-through | Comma-separated: case A, B, C |
| Verbosity | High (break on every case) | Low (arrow syntax is concise) |
The first major change is the introduction of the arrow syntax (->) for case labels. Instead of case X: (colon form), you write case X -> (arrow form). The arrow form eliminates fall-through entirely. When execution enters an arrow case, it runs only that case’s code and then exits the switch. No break needed. No fall-through possible.
public class ArrowLabelsDemo {
public static void main(String[] args) {
String day = "WEDNESDAY";
// Traditional colon syntax -- fall-through is possible
System.out.println("=== Traditional ===");
switch (day) {
case "MONDAY":
System.out.println("Start of work week");
break;
case "WEDNESDAY":
System.out.println("Midweek");
break;
case "FRIDAY":
System.out.println("Almost weekend!");
break;
default:
System.out.println("Regular day");
break;
}
// Arrow syntax -- no fall-through, no break needed
System.out.println("=== Arrow ===");
switch (day) {
case "MONDAY" -> System.out.println("Start of work week");
case "WEDNESDAY" -> System.out.println("Midweek");
case "FRIDAY" -> System.out.println("Almost weekend!");
default -> System.out.println("Regular day");
}
// Arrow with block bodies -- use curly braces for multiple statements
switch (day) {
case "MONDAY" -> {
System.out.println("Monday");
System.out.println("Time to plan the week");
}
case "FRIDAY" -> {
System.out.println("Friday");
System.out.println("Time to wrap up");
}
default -> System.out.println("Regular day: " + day);
}
}
}
Key rules for arrow labels:
break needed (and you should not use it)case X -> { ... }public class NoMixingDemo {
public static void main(String[] args) {
String day = "MONDAY";
// COMPILE ERROR: cannot mix arrow and colon labels
// switch (day) {
// case "MONDAY" -> System.out.println("Monday");
// case "TUESDAY":
// System.out.println("Tuesday");
// break;
// }
// Pick one style and stick with it in each switch
}
}
This is the game-changer. A switch expression produces a value, just like a ternary operator or a method call. You can assign the result of a switch directly to a variable, return it from a method, or pass it as an argument. No more declaring a variable before the switch and assigning it in each case.
public class SwitchExpressionDemo {
public static void main(String[] args) {
String day = "WEDNESDAY";
// OLD WAY: declare variable, assign in each case
String dayType;
switch (day) {
case "MONDAY":
case "TUESDAY":
case "WEDNESDAY":
case "THURSDAY":
case "FRIDAY":
dayType = "Weekday";
break;
case "SATURDAY":
case "SUNDAY":
dayType = "Weekend";
break;
default:
dayType = "Unknown";
break;
}
// NEW WAY: switch expression assigns directly
String dayTypeNew = switch (day) {
case "MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY" -> "Weekday";
case "SATURDAY", "SUNDAY" -> "Weekend";
default -> "Unknown";
}; // Note the semicolon -- the switch expression is part of an assignment statement
System.out.println(dayType); // Weekday
System.out.println(dayTypeNew); // Weekday
// Using switch expression in a return statement
System.out.println(categorize(85));
System.out.println(categorize(42));
// Using switch expression as a method argument
System.out.println("Grade: " + switch (95) {
case 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100 -> "A";
default -> "Other";
});
// Using switch expression in a calculation
int month = 6;
int daysInMonth = switch (month) {
case 1, 3, 5, 7, 8, 10, 12 -> 31;
case 4, 6, 9, 11 -> 30;
case 2 -> 28; // simplified, ignoring leap years
default -> throw new IllegalArgumentException("Invalid month: " + month);
};
System.out.println("Days in month " + month + ": " + daysInMonth);
}
static String categorize(int score) {
return switch (score / 10) {
case 10, 9 -> "Excellent";
case 8 -> "Good";
case 7 -> "Average";
case 6 -> "Below Average";
default -> "Needs Improvement";
};
}
}
Critical detail: When a switch is used as an expression (i.e., its result is assigned, returned, or used), it must be exhaustive. Every possible input value must be handled. If the compiler cannot verify exhaustiveness, you must include a default case. More on this in section 6.
Also note the semicolon after the closing brace when a switch expression is part of a statement. This is easy to forget:
// The semicolon terminates the assignment statement, not the switch
int result = switch (x) {
case 1 -> 10;
case 2 -> 20;
default -> 0;
}; // <-- This semicolon is required!
// Same as writing:
// int result = someMethodThatReturnsInt();
// ^ semicolon terminates the statement
When an arrow case needs to compute a value through multiple statements, you use the yield keyword to return the value from the block. Think of yield as the switch-expression equivalent of return -- it specifies the value that the case produces.
When do you need yield? Only when you have a block body (curly braces) in a switch expression. Single-expression arrow cases produce their value directly. Statement switches (not used as expressions) do not need yield at all.
public class YieldDemo {
public static void main(String[] args) {
int month = 3;
int year = 2024;
// Simple arrow cases -- no yield needed, the expression IS the value
int daysSimple = switch (month) {
case 1, 3, 5, 7, 8, 10, 12 -> 31;
case 4, 6, 9, 11 -> 30;
case 2 -> 28;
default -> throw new IllegalArgumentException("Invalid month");
};
// Block body -- yield IS needed to produce the value
int daysComplex = switch (month) {
case 1, 3, 5, 7, 8, 10, 12 -> {
System.out.println("31-day month");
yield 31;
}
case 4, 6, 9, 11 -> {
System.out.println("30-day month");
yield 30;
}
case 2 -> {
boolean isLeap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
System.out.println(isLeap ? "Leap year February" : "Regular February");
yield isLeap ? 29 : 28;
}
default -> throw new IllegalArgumentException("Invalid month: " + month);
};
System.out.println("Days in month " + month + ": " + daysComplex);
// yield also works with colon-style cases in switch expressions
String season = switch (month) {
case 12, 1, 2:
yield "Winter";
case 3, 4, 5:
yield "Spring";
case 6, 7, 8:
yield "Summer";
case 9, 10, 11:
yield "Fall";
default:
yield "Unknown";
};
System.out.println("Season: " + season);
}
}
yield vs return:
| Keyword | Context | Effect |
|---|---|---|
return |
Inside a method | Exits the method and returns a value to the caller |
yield |
Inside a switch expression block | Produces the value for that case without exiting the method |
break |
Inside a traditional switch statement | Exits the switch (no value produced) |
Important: yield is a context-sensitive keyword, not a reserved word. You can still have variables, methods, or classes named yield (though you probably should not). It only has special meaning inside a switch expression block.
public class YieldNotReserved {
public static void main(String[] args) {
// "yield" is not a reserved word -- this compiles fine
int yield = 42;
System.out.println(yield); // 42
// But inside a switch expression, "yield" has special meaning
int result = switch (yield) {
case 42 -> {
int computed = yield * 2; // "yield" here is the variable
yield computed; // "yield" here is the keyword
}
default -> 0;
};
System.out.println(result); // 84
}
}
In the traditional switch, handling multiple values with the same logic required stacking case labels using fall-through:
public class MultipleCaseLabels {
public static void main(String[] args) {
int statusCode = 404;
// OLD WAY: stacked cases relying on fall-through
String categoryOld;
switch (statusCode) {
case 200:
case 201:
case 202:
case 204:
categoryOld = "Success";
break;
case 301:
case 302:
case 307:
case 308:
categoryOld = "Redirect";
break;
case 400:
case 401:
case 403:
case 404:
case 422:
categoryOld = "Client Error";
break;
case 500:
case 502:
case 503:
case 504:
categoryOld = "Server Error";
break;
default:
categoryOld = "Unknown";
break;
}
// NEW WAY: comma-separated case labels -- clean and explicit
String categoryNew = switch (statusCode) {
case 200, 201, 202, 204 -> "Success";
case 301, 302, 307, 308 -> "Redirect";
case 400, 401, 403, 404, 422 -> "Client Error";
case 500, 502, 503, 504 -> "Server Error";
default -> "Unknown";
};
System.out.println(categoryOld); // Client Error
System.out.println(categoryNew); // Client Error
// Multiple labels work with strings too
String fruit = "apple";
String color = switch (fruit) {
case "apple", "cherry", "strawberry" -> "Red";
case "banana", "lemon", "pineapple" -> "Yellow";
case "lime", "kiwi", "avocado" -> "Green";
case "blueberry", "grape", "plum" -> "Purple";
case "orange", "tangerine", "mango" -> "Orange";
default -> "Unknown color";
};
System.out.println(fruit + " is " + color); // apple is Red
}
}
The comma-separated syntax communicates intent far better than stacked fall-through cases. When you read case 200, 201, 202, 204 ->, you immediately understand that all these values lead to the same result. With the traditional syntax, you have to mentally verify that there is no code between the stacked cases -- any statement would change the behavior.
When switch is used as an expression (its value is assigned, returned, or used), the compiler requires it to be exhaustive. This means every possible value of the selector must be handled by some case. If the compiler cannot prove exhaustiveness, the code does not compile.
This is a major safety improvement. The traditional switch statement was happy to silently do nothing if no case matched. A switch expression forces you to handle every possibility or explicitly acknowledge unknown values with default.
public class ExhaustivenessDemo {
public static void main(String[] args) {
int value = 5;
// COMPILE ERROR: switch expression must be exhaustive
// String label = switch (value) {
// case 1 -> "One";
// case 2 -> "Two";
// case 3 -> "Three";
// // Missing default! int has ~4 billion possible values
// };
// CORRECT: add default to handle remaining values
String label = switch (value) {
case 1 -> "One";
case 2 -> "Two";
case 3 -> "Three";
default -> "Other: " + value;
};
System.out.println(label); // Other: 5
// Switch STATEMENTS (not expressions) are NOT required to be exhaustive
// This compiles fine, even though it does not handle all ints:
switch (value) {
case 1 -> System.out.println("One");
case 2 -> System.out.println("Two");
// no default -- and that is fine for statements
}
// You can use default to throw an exception for unexpected values
char grade = 'B';
double gradePoints = switch (grade) {
case 'A' -> 4.0;
case 'B' -> 3.0;
case 'C' -> 2.0;
case 'D' -> 1.0;
case 'F' -> 0.0;
default -> throw new IllegalArgumentException("Invalid grade: " + grade);
};
System.out.println("Grade points: " + gradePoints);
}
}
Exhaustiveness rules:
| Selector Type | Exhaustive Without Default? | Notes |
|---|---|---|
int, short, byte, char |
No (too many values) | Always need default |
String |
No (infinite values) | Always need default |
enum |
Yes, if all constants covered | No default needed (but recommended) |
| Sealed class (Java 17+) | Yes, if all permitted subtypes covered | Works with pattern matching (Java 21) |
Enums and switch expressions are a natural fit. Since an enum has a fixed set of constants, the compiler can verify that your switch handles all of them. If you cover every enum constant, you do not need a default case. And if someone later adds a new constant to the enum, the compiler will flag every switch expression that does not handle it. This is exactly the kind of compile-time safety that prevents production bugs.
public class SwitchWithEnums {
enum Season { SPRING, SUMMER, FALL, WINTER }
enum HttpMethod { GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS }
enum Priority { LOW, MEDIUM, HIGH, CRITICAL }
public static void main(String[] args) {
// Exhaustive without default -- all enum constants covered
Season season = Season.SUMMER;
String activity = switch (season) {
case SPRING -> "Gardening";
case SUMMER -> "Swimming";
case FALL -> "Hiking";
case WINTER -> "Skiing";
// No default needed! All 4 constants are covered.
};
System.out.println(season + ": " + activity);
// If you add a new Season constant and forget to update the switch,
// the compiler will give you an error:
// "the switch expression does not cover all possible input values"
// HTTP method handling
HttpMethod method = HttpMethod.POST;
String action = switch (method) {
case GET -> "Retrieving resource";
case POST -> "Creating resource";
case PUT -> "Replacing resource";
case PATCH -> "Updating resource";
case DELETE -> "Deleting resource";
case HEAD -> "Checking resource headers";
case OPTIONS -> "Listing available methods";
};
System.out.println(method + ": " + action);
// Enum with yield for complex logic
Priority priority = Priority.CRITICAL;
int responseTimeMinutes = switch (priority) {
case LOW -> 1440; // 24 hours
case MEDIUM -> 240; // 4 hours
case HIGH -> 60; // 1 hour
case CRITICAL -> {
System.out.println("ALERT: Critical priority detected!");
System.out.println("Paging on-call engineer...");
yield 15; // 15 minutes
}
};
System.out.println("Response time: " + responseTimeMinutes + " minutes");
}
}
This is a nuanced topic. If you cover all enum constants, the compiler does not require default. However, there is a case for including it defensively:
public class EnumDefaultDebate {
enum Color { RED, GREEN, BLUE }
public static void main(String[] args) {
Color color = Color.RED;
// Option 1: No default -- compiler ensures all cases covered
// If someone adds YELLOW to the enum, THIS code won't compile until updated
String hex1 = switch (color) {
case RED -> "#FF0000";
case GREEN -> "#00FF00";
case BLUE -> "#0000FF";
};
// Option 2: With default -- handles future enum constants at runtime
// If someone adds YELLOW, this code still compiles but throws at runtime
String hex2 = switch (color) {
case RED -> "#FF0000";
case GREEN -> "#00FF00";
case BLUE -> "#0000FF";
default -> throw new IllegalArgumentException("Unhandled color: " + color);
};
// RECOMMENDATION: Omit default for enum switch expressions.
// A compile error (Option 1) is ALWAYS better than a runtime error (Option 2).
// The compile error forces you to handle the new case immediately.
// The runtime error only surfaces when that code path actually executes.
}
}
Switch expressions work with all the types that traditional switch supports: int, byte, short, char, String, and enum types. For these types, a default case is required when used as an expression (since you cannot enumerate all possible Strings or ints).
public class SwitchWithStringsAndNumbers {
public static void main(String[] args) {
// === Switch on String ===
String command = "deploy";
String action = switch (command.toLowerCase()) {
case "build" -> "Compiling source code...";
case "test" -> "Running test suite...";
case "deploy" -> "Deploying to production...";
case "rollback" -> "Rolling back last deployment...";
case "status" -> "Checking system status...";
default -> "Unknown command: " + command;
};
System.out.println(action);
// === Switch on int ===
int httpStatus = 201;
String message = switch (httpStatus) {
case 200 -> "OK";
case 201 -> "Created";
case 204 -> "No Content";
case 301 -> "Moved Permanently";
case 302 -> "Found (Redirect)";
case 400 -> "Bad Request";
case 401 -> "Unauthorized";
case 403 -> "Forbidden";
case 404 -> "Not Found";
case 500 -> "Internal Server Error";
case 502 -> "Bad Gateway";
case 503 -> "Service Unavailable";
default -> "HTTP " + httpStatus;
};
System.out.println(httpStatus + ": " + message);
// === Switch on char ===
char operator = '+';
double result = switch (operator) {
case '+' -> 10.0 + 5.0;
case '-' -> 10.0 - 5.0;
case '*' -> 10.0 * 5.0;
case '/' -> 10.0 / 5.0;
case '%' -> 10.0 % 5.0;
default -> throw new IllegalArgumentException("Unknown operator: " + operator);
};
System.out.println("10 " + operator + " 5 = " + result);
// === Range-based switching using int division ===
int score = 87;
String grade = switch (score / 10) {
case 10, 9 -> "A";
case 8 -> "B";
case 7 -> "C";
case 6 -> "D";
default -> "F";
};
System.out.println("Score " + score + " = Grade " + grade);
}
}
Java 17 includes pattern matching for switch as a preview feature (JEP 406). This allows switching on types and destructuring objects directly in case labels. While it is not yet a permanent feature in Java 17, it becomes standard in Java 21. Here is a quick preview of what it looks like:
// PREVIEW FEATURE in Java 17 -- requires --enable-preview flag
// Becomes standard in Java 21
// Pattern matching allows switching on types:
// static String describe(Object obj) {
// return switch (obj) {
// case Integer i -> "Integer: " + i;
// case String s -> "String of length " + s.length();
// case int[] arr -> "int array of length " + arr.length;
// case null -> "null value";
// default -> "Other: " + obj.getClass().getName();
// };
// }
// For Java 17 production code, stick with standard switch expressions
// Pattern matching will be covered in a dedicated Java 21 tutorial
Historically, passing null to a switch statement throws a NullPointerException before any case is evaluated. This behavior has not changed in Java 17 for standard switch expressions. The NPE is thrown at the point where the switch evaluates its selector, not inside any case.
public class NullHandlingDemo {
public static void main(String[] args) {
// Traditional switch: NPE on null
String value = null;
try {
// This throws NullPointerException BEFORE entering any case
switch (value) {
case "A":
System.out.println("A");
break;
default:
System.out.println("Default");
}
} catch (NullPointerException e) {
System.out.println("NPE from traditional switch: " + e.getMessage());
}
// Switch expression: same behavior -- NPE on null
try {
String result = switch (value) {
case "A" -> "Letter A";
case "B" -> "Letter B";
default -> "Other";
};
} catch (NullPointerException e) {
System.out.println("NPE from switch expression: " + e.getMessage());
}
// BEST PRACTICE: Guard against null BEFORE the switch
String safeResult = handleCommand(null);
System.out.println(safeResult);
safeResult = handleCommand("start");
System.out.println(safeResult);
}
// Defensive approach: null check before switch
static String handleCommand(String command) {
if (command == null) {
return "Error: command cannot be null";
}
return switch (command) {
case "start" -> "Starting...";
case "stop" -> "Stopping...";
case "restart" -> "Restarting...";
default -> "Unknown command: " + command;
};
}
// Alternative: use Objects.requireNonNull for fail-fast
static String processInput(String input) {
java.util.Objects.requireNonNull(input, "Input must not be null");
return switch (input) {
case "yes", "y" -> "Confirmed";
case "no", "n" -> "Rejected";
default -> "Invalid input: " + input;
};
}
}
Note on Java 21+: Starting with Java 21, you can handle null directly as a case label in switch: case null -> "null value". In Java 17, this is only available as a preview feature. For production Java 17 code, always guard against null before the switch.
Let us put all three switch forms side by side so you can see the progression from the oldest style to the most modern.
| Feature | Traditional (Colon + Break) | Arrow Statement | Switch Expression |
|---|---|---|---|
| Syntax | case X: |
case X -> |
case X -> (used as expression) |
| Fall-through | Yes (default) | No | No |
| Returns value | No | No (statement form) | Yes |
| Requires break | Yes | No | No |
| Exhaustive | No | No (statement form) | Yes (compiler-enforced) |
| Multiple labels | Stacked fall-through | Comma-separated | Comma-separated |
| Block body | Colon cases share scope | Curly braces: { } |
Curly braces with yield |
| Recommended | Legacy code only | When no value needed | Default choice |
Here is the same logic written in all three styles:
public class ThreeWayComparison {
enum Direction { NORTH, SOUTH, EAST, WEST }
public static void main(String[] args) {
Direction dir = Direction.EAST;
// ========================================
// STYLE 1: Traditional (colon + break)
// ========================================
String result1;
switch (dir) {
case NORTH:
result1 = "Moving up";
break;
case SOUTH:
result1 = "Moving down";
break;
case EAST:
result1 = "Moving right";
break;
case WEST:
result1 = "Moving left";
break;
default:
result1 = "Unknown";
break;
}
System.out.println("Traditional: " + result1);
// ========================================
// STYLE 2: Arrow statement (no value)
// ========================================
switch (dir) {
case NORTH -> System.out.println("Arrow: Moving up");
case SOUTH -> System.out.println("Arrow: Moving down");
case EAST -> System.out.println("Arrow: Moving right");
case WEST -> System.out.println("Arrow: Moving left");
}
// ========================================
// STYLE 3: Switch expression (returns value)
// ========================================
String result3 = switch (dir) {
case NORTH -> "Moving up";
case SOUTH -> "Moving down";
case EAST -> "Moving right";
case WEST -> "Moving left";
};
System.out.println("Expression: " + result3);
}
}
The progression is clear: Style 3 is the most concise, the safest (exhaustive, no fall-through), and the most expressive (produces a value). Use it as your default choice.
Let us look at practical, production-quality examples that demonstrate how switch expressions clean up real application code.
public class Calculator {
enum Operation { ADD, SUBTRACT, MULTIPLY, DIVIDE, MODULO, POWER }
public static double calculate(double a, Operation op, double b) {
return switch (op) {
case ADD -> a + b;
case SUBTRACT -> a - b;
case MULTIPLY -> a * b;
case DIVIDE -> {
if (b == 0) {
throw new ArithmeticException("Cannot divide by zero");
}
yield a / b;
}
case MODULO -> {
if (b == 0) {
throw new ArithmeticException("Cannot modulo by zero");
}
yield a % b;
}
case POWER -> Math.pow(a, b);
};
}
public static String formatResult(double a, Operation op, double b) {
String symbol = switch (op) {
case ADD -> "+";
case SUBTRACT -> "-";
case MULTIPLY -> "*";
case DIVIDE -> "/";
case MODULO -> "%";
case POWER -> "^";
};
double result = calculate(a, op, b);
return "%.2f %s %.2f = %.2f".formatted(a, symbol, b, result);
}
public static void main(String[] args) {
System.out.println(formatResult(10, Operation.ADD, 5)); // 10.00 + 5.00 = 15.00
System.out.println(formatResult(10, Operation.SUBTRACT, 3)); // 10.00 - 3.00 = 7.00
System.out.println(formatResult(10, Operation.MULTIPLY, 4)); // 10.00 * 4.00 = 40.00
System.out.println(formatResult(10, Operation.DIVIDE, 3)); // 10.00 / 3.00 = 3.33
System.out.println(formatResult(10, Operation.MODULO, 3)); // 10.00 % 3.00 = 1.00
System.out.println(formatResult(2, Operation.POWER, 10)); // 2.00 ^ 10.00 = 1024.00
// Division by zero
try {
System.out.println(formatResult(10, Operation.DIVIDE, 0));
} catch (ArithmeticException e) {
System.out.println("Error: " + e.getMessage());
}
}
}
public class HttpStatusHandler {
record HttpResponse(int statusCode, String body) {}
enum StatusCategory { INFORMATIONAL, SUCCESS, REDIRECT, CLIENT_ERROR, SERVER_ERROR }
public static StatusCategory categorize(int code) {
return switch (code / 100) {
case 1 -> StatusCategory.INFORMATIONAL;
case 2 -> StatusCategory.SUCCESS;
case 3 -> StatusCategory.REDIRECT;
case 4 -> StatusCategory.CLIENT_ERROR;
case 5 -> StatusCategory.SERVER_ERROR;
default -> throw new IllegalArgumentException("Invalid HTTP status code: " + code);
};
}
public static String handleResponse(HttpResponse response) {
return switch (categorize(response.statusCode())) {
case INFORMATIONAL -> {
yield "Info [%d]: Processing continues".formatted(response.statusCode());
}
case SUCCESS -> {
yield "Success [%d]: %s".formatted(
response.statusCode(),
response.body() != null ? response.body() : "No content"
);
}
case REDIRECT -> {
yield "Redirect [%d]: Following redirect...".formatted(response.statusCode());
}
case CLIENT_ERROR -> {
String advice = switch (response.statusCode()) {
case 400 -> "Check request syntax";
case 401 -> "Authentication required";
case 403 -> "Access denied - check permissions";
case 404 -> "Resource not found - verify URL";
case 429 -> "Rate limited - retry after backoff";
default -> "Client error";
};
yield "Error [%d]: %s".formatted(response.statusCode(), advice);
}
case SERVER_ERROR -> {
String advice = switch (response.statusCode()) {
case 500 -> "Internal error - check server logs";
case 502 -> "Bad gateway - upstream server issue";
case 503 -> "Service unavailable - retry later";
case 504 -> "Gateway timeout - upstream too slow";
default -> "Server error";
};
yield "Critical [%d]: %s".formatted(response.statusCode(), advice);
}
};
}
public static void main(String[] args) {
HttpResponse[] responses = {
new HttpResponse(200, "{\"status\": \"ok\"}"),
new HttpResponse(201, "{\"id\": 42}"),
new HttpResponse(301, null),
new HttpResponse(400, "Invalid JSON"),
new HttpResponse(404, null),
new HttpResponse(500, "NullPointerException"),
new HttpResponse(503, null)
};
for (HttpResponse resp : responses) {
System.out.println(handleResponse(resp));
}
}
}
public class OrderStateMachine {
enum OrderState { CREATED, CONFIRMED, PROCESSING, SHIPPED, DELIVERED, CANCELLED }
enum OrderEvent { CONFIRM, PAY, SHIP, DELIVER, CANCEL }
// State transition function using switch expressions
public static OrderState transition(OrderState current, OrderEvent event) {
return switch (current) {
case CREATED -> switch (event) {
case CONFIRM -> OrderState.CONFIRMED;
case CANCEL -> OrderState.CANCELLED;
default -> throw new IllegalStateException(
"Cannot %s order in %s state".formatted(event, current));
};
case CONFIRMED -> switch (event) {
case PAY -> OrderState.PROCESSING;
case CANCEL -> OrderState.CANCELLED;
default -> throw new IllegalStateException(
"Cannot %s order in %s state".formatted(event, current));
};
case PROCESSING -> switch (event) {
case SHIP -> OrderState.SHIPPED;
case CANCEL -> OrderState.CANCELLED;
default -> throw new IllegalStateException(
"Cannot %s order in %s state".formatted(event, current));
};
case SHIPPED -> switch (event) {
case DELIVER -> OrderState.DELIVERED;
default -> throw new IllegalStateException(
"Cannot %s shipped order".formatted(event));
};
case DELIVERED, CANCELLED -> throw new IllegalStateException(
"Order in %s state is final -- no transitions allowed".formatted(current));
};
}
public static void main(String[] args) {
// Happy path
OrderState state = OrderState.CREATED;
System.out.println("Initial: " + state);
state = transition(state, OrderEvent.CONFIRM);
System.out.println("Confirmed: " + state);
state = transition(state, OrderEvent.PAY);
System.out.println("Processing: " + state);
state = transition(state, OrderEvent.SHIP);
System.out.println("Shipped: " + state);
state = transition(state, OrderEvent.DELIVER);
System.out.println("Delivered: " + state);
// Invalid transition
try {
transition(state, OrderEvent.CANCEL);
} catch (IllegalStateException e) {
System.out.println("Error: " + e.getMessage());
}
// Cancellation path
OrderState order2 = OrderState.CREATED;
order2 = transition(order2, OrderEvent.CONFIRM);
order2 = transition(order2, OrderEvent.CANCEL);
System.out.println("\nCancelled order: " + order2);
}
}
import java.util.*;
public class CommandProcessor {
record Command(String name, List args) {
static Command parse(String input) {
String[] parts = input.trim().split("\\s+");
String name = parts[0].toLowerCase();
List args = parts.length > 1
? List.of(Arrays.copyOfRange(parts, 1, parts.length))
: List.of();
return new Command(name, args);
}
}
record CommandResult(boolean success, String message) {}
// Process commands using switch expressions
public static CommandResult execute(Command cmd) {
return switch (cmd.name()) {
case "help" -> new CommandResult(true, switch (cmd.args().size()) {
case 0 -> """
Available commands:
help [command] - Show help
list - List items
add - - Add an item
remove
- - Remove an item
search
- Search items
clear - Clear all items
exit - Exit the program""";
default -> {
String topic = cmd.args().get(0);
yield switch (topic) {
case "add" -> "Usage: add - - Adds an item to the list";
case "remove" -> "Usage: remove
- - Removes an item from the list";
case "search" -> "Usage: search
- Searches items by name";
default -> "No help available for: " + topic;
};
}
});
case "list" -> new CommandResult(true, "Items: [item1, item2, item3]");
case "add" -> {
if (cmd.args().isEmpty()) {
yield new CommandResult(false, "Error: 'add' requires an item name");
}
String item = String.join(" ", cmd.args());
yield new CommandResult(true, "Added: " + item);
}
case "remove" -> {
if (cmd.args().isEmpty()) {
yield new CommandResult(false, "Error: 'remove' requires an item name");
}
String item = String.join(" ", cmd.args());
yield new CommandResult(true, "Removed: " + item);
}
case "search" -> {
if (cmd.args().isEmpty()) {
yield new CommandResult(false, "Error: 'search' requires a query");
}
String query = String.join(" ", cmd.args());
yield new CommandResult(true, "Search results for '" + query + "': [item1, item3]");
}
case "clear" -> new CommandResult(true, "All items cleared");
case "exit" -> new CommandResult(true, "Goodbye!");
default -> new CommandResult(false,
"Unknown command: '%s'. Type 'help' for available commands.".formatted(cmd.name()));
};
}
public static void main(String[] args) {
String[] inputs = {
"help",
"help add",
"add Buy groceries",
"list",
"search groceries",
"remove Buy groceries",
"add",
"unknown command",
"exit"
};
for (String input : inputs) {
Command cmd = Command.parse(input);
CommandResult result = execute(cmd);
String status = result.success() ? "OK" : "ERROR";
System.out.printf("[%s] > %s%n", status, input);
System.out.println(result.message());
System.out.println();
}
}
}
After working with switch expressions across production Java 17 codebases, here are the guidelines that lead to clean, maintainable, and bug-free code.
Unless you have a specific reason to use the colon form (e.g., intentional fall-through in a switch statement), always use the arrow syntax. It eliminates the most common source of switch bugs (missing break) and is more concise.
If a switch's purpose is to produce a value, make it an expression. This gives you exhaustiveness checking and eliminates the need to declare and assign variables separately.
public class BestPracticesDemo {
enum LogLevel { TRACE, DEBUG, INFO, WARN, ERROR, FATAL }
public static void main(String[] args) {
LogLevel level = LogLevel.WARN;
// BAD: switch statement to compute a value
String colorBad;
switch (level) {
case TRACE, DEBUG -> colorBad = "GRAY";
case INFO -> colorBad = "GREEN";
case WARN -> colorBad = "YELLOW";
case ERROR -> colorBad = "RED";
case FATAL -> colorBad = "RED_BOLD";
}
// GOOD: switch expression -- cleaner and compiler-checked
String colorGood = switch (level) {
case TRACE, DEBUG -> "GRAY";
case INFO -> "GREEN";
case WARN -> "YELLOW";
case ERROR -> "RED";
case FATAL -> "RED_BOLD";
};
System.out.println(colorGood);
}
}
Prefer single-expression arrow cases. Only use block bodies with yield when you genuinely need multiple statements -- validation, logging, intermediate computation.
For enum switches, do not add a default case. Let the compiler enforce that all enum constants are handled. This way, if someone adds a new constant, you get a compile error -- not a runtime bug discovered weeks later in production.
For non-enum types (String, int), always have a default case. When the default represents truly unexpected input, throw an exception rather than returning a neutral value. Silent failures are worse than loud failures.
If a case body grows beyond 5-10 lines, extract it into a private method. The switch should read like a routing table -- easy to scan.
public class KeepCasesSimple {
enum RequestType { CREATE, READ, UPDATE, DELETE }
record Request(RequestType type, String payload) {}
record Response(int status, String body) {}
// GOOD: Cases delegate to focused methods
static Response handle(Request request) {
return switch (request.type()) {
case CREATE -> handleCreate(request.payload());
case READ -> handleRead(request.payload());
case UPDATE -> handleUpdate(request.payload());
case DELETE -> handleDelete(request.payload());
};
}
private static Response handleCreate(String payload) {
// Validation, business logic, persistence...
System.out.println("Creating resource: " + payload);
return new Response(201, "Created");
}
private static Response handleRead(String payload) {
System.out.println("Reading resource: " + payload);
return new Response(200, "{ \"id\": 1, \"name\": \"item\" }");
}
private static Response handleUpdate(String payload) {
System.out.println("Updating resource: " + payload);
return new Response(200, "Updated");
}
private static Response handleDelete(String payload) {
System.out.println("Deleting resource: " + payload);
return new Response(204, "");
}
public static void main(String[] args) {
Request req = new Request(RequestType.CREATE, "{ \"name\": \"Widget\" }");
Response resp = handle(req);
System.out.println("Status: " + resp.status() + ", Body: " + resp.body());
}
}
->) should be your default -- no fall-through, no breakcase A, B, C ->) replace fall-through stacking -- more explicit and readabledefault for enums to get compile-time safetySwitch expressions are one of the best quality-of-life improvements in modern Java. They eliminate an entire class of bugs (fall-through), reduce boilerplate (no break statements), and give you compiler-enforced completeness. If you are still writing traditional switch statements in Java 17 code, now is the time to modernize. Every new switch you write should be an expression with arrow syntax unless you have a compelling reason otherwise.