Java 17 Switch Expressions

1. Introduction

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:

  • Verbosity — Every case needs a break statement, bloating the code
  • Not an expression — You cannot assign the result of a switch to a variable directly, so you must declare the variable before the switch and assign it inside each case
  • No exhaustiveness checking — The compiler does not warn you if you forgot a case (except for enums in some IDEs)
  • Scope leaks — Variables declared in one case are visible in subsequent cases without curly braces

Switch 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)

2. Arrow Labels

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:

  • No fall-through — each case runs independently
  • No break needed (and you should not use it)
  • Single expression or statement to the right of the arrow
  • For multiple statements, use a block with curly braces: case X -> { ... }
  • You cannot mix arrow and colon forms in the same switch — pick one
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
    }
}

3. Switch as Expression

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

4. The yield Keyword

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
    }
}

5. Multiple Case Labels

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.

6. Exhaustiveness

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)

7. Switch with Enums

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");
    }
}

Should You Add default to Enum Switches?

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.
    }
}

8. Switch with Strings and Numbers

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);
    }
}

A Note on Pattern Matching (Preview in Java 17)

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

9. Null Handling

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.

10. Comparison: Traditional vs Arrow vs Expression

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.

11. Real-World Examples

Let us look at practical, production-quality examples that demonstrate how switch expressions clean up real application code.

Example 1: Calculator

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());
        }
    }
}

Example 2: HTTP Status Handler

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));
        }
    }
}

Example 3: State Machine

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);
    }
}

Example 4: Command Processor

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();
        }
    }
}

12. Best Practices

After working with switch expressions across production Java 17 codebases, here are the guidelines that lead to clean, maintainable, and bug-free code.

1. Default to Arrow Syntax

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.

2. Prefer Switch Expressions Over Statements

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);
    }
}

3. Use yield Only When Necessary

Prefer single-expression arrow cases. Only use block bodies with yield when you genuinely need multiple statements -- validation, logging, intermediate computation.

4. Leverage Exhaustiveness

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.

5. Throw in Default for Unexpected Values

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.

6. Keep Cases Simple

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());
    }
}

Summary of Key Rules

  • Arrow syntax (->) should be your default -- no fall-through, no break
  • Switch expressions produce values and enforce exhaustiveness -- use them when computing a result
  • yield returns a value from a block body in a switch expression -- use it only when you need multiple statements
  • Comma-separated labels (case A, B, C ->) replace fall-through stacking -- more explicit and readable
  • Exhaustiveness is your friend -- omit default for enums to get compile-time safety
  • Null guard before the switch -- in Java 17, null still throws NPE
  • Keep cases short -- extract complex logic into methods
  • Throw on unexpected values -- do not silently return defaults for input you did not anticipate

Switch 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.




Subscribe To Our Newsletter
You will receive our latest post and tutorial.
Thank you for subscribing!

required
required


Leave a Reply

Your email address will not be published. Required fields are marked *