Switch Expressions

Map a value to a result with arrow-form switch expressions, yield and case null, without the fall-through bugs of the old switch statement.

SyntaxType result = switch (value) { case A, B -> expression; case C -> { ...; yield expression; } default -> expression; };

A switch expression compares one value against a list of cases and produces a result. Since Java 14 it is the normal way to turn an enum, string or number into something else: an HTTP status, a label, a SQL fragment.

int status = switch (error) {
    case "not_found" -> 404;
    case "invalid_input", "missing_field" -> 400;
    default -> 500;
};

Arrow cases do not fall through

The old colon form (case "a":) runs every following case until it meets a break. Forgetting one is a classic bug: the role "editor" ends up with the permissions of "viewer". The arrow form (case "a" ->) runs exactly one case. List several labels in one case with commas instead of stacking empty cases.

yield for multi-line cases

A case can run a block. Inside a switch expression the block must end with yield value;, which is what the whole expression evaluates to. return inside that block is a compile error, because it would leave the method instead of producing a value. A case may also throw, which is how you reject unknown input.

Exhaustiveness

A switch expression must cover every possible value. For strings and numbers that means a default. For an enum you can list every constant and leave default out; then adding a new constant to the enum makes every switch that misses it fail to compile, which is exactly the reminder you want when an order gains a new REFUNDED state. The same check applies to sealed interfaces with pattern cases (see Pattern Matching in switch).

null

Switching on a null string or enum throws a NullPointerException, even if there is a default. Since Java 21 you can add case null, or combine it as case null, default ->. That is useful for optional query parameters such as ?sort=, where a missing value should pick a default ordering.

Things to know

  • String cases are compared with equals, so they are case-sensitive: "get" does not match "GET". Normalise input first.
  • Case labels must be constants. Use an if chain when the conditions are ranges such as amount > 1000, or use a pattern with a when guard.
  • A switch statement with arrow cases is fine when you only want side effects and no value.

For mapping error types to status codes, order states to labels or commands to handlers, a switch expression keeps the mapping in one readable table that the compiler checks for gaps.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Map domain error codes to HTTP status codes in one place in an exception handler.
  • Turn a ?sort= query parameter into a fixed ORDER BY clause, rejecting anything unknown.
  • Show a label for each order status and get a compile error when a new status is added.

More examples

The fall-through bug the arrow form removes

The colon form starts at the matching case and keeps going through every case below it. The arrow form runs only the matching case, so the editor gets write access as intended.

Example · java
void main() {
    String role = "editor";

    String access = "none";
    switch (role) {
        case "admin":
            access = "all";
        case "editor":
            access = "write";
        case "viewer":
            access = "read"; // reached because nothing above has a break
    }
    IO.println("colon form: " + access);

    String fixed = switch (role) {
        case "admin" -> "all";
        case "editor" -> "write";
        case "viewer" -> "read";
        default -> "none";
    };
    IO.println("arrow form: " + fixed);
}
// Output:
// colon form: read
// arrow form: write

case null for an optional query parameter

Mapping user input to a fixed set of SQL fragments means the raw value never reaches the query. Without case null, a missing ?sort parameter would throw NullPointerException before any case was tested.

Example · java
String orderBy(String sort) {
    return switch (sort) {
        case null -> "created_at DESC";
        case "newest" -> "created_at DESC";
        case "oldest" -> "created_at ASC";
        case "price" -> "price ASC";
        default -> throw new IllegalArgumentException("unknown sort: " + sort);
    };
}

void main() {
    IO.println(orderBy(null));
    IO.println(orderBy("price"));
    try {
        IO.println(orderBy("name; DROP TABLE orders"));
    } catch (IllegalArgumentException e) {
        IO.println("400: " + e.getMessage());
    }
}
// Output:
// created_at DESC
// price ASC
// 400: unknown sort: name; DROP TABLE orders

Switching on a sealed result type

Because Result is sealed, the compiler knows every subtype and needs no default. The when guard turns an empty Ok into 204 No Content, and _ (final since Java 22) marks a pattern variable that is not used.

Example · java
sealed interface Result {}
record Ok(String body) implements Result {}
record NotFound(String id) implements Result {}
record Invalid(List<String> errors) implements Result {}

int status(Result r) {
    return switch (r) {
        case Ok ok when ok.body().isEmpty() -> 204;
        case Ok _ -> 200;
        case NotFound _ -> 404;
        case Invalid _ -> 400;
    };
}

void main() {
    IO.println(status(new Ok("{}")));
    IO.println(status(new Ok("")));
    IO.println(status(new NotFound("42")));
    IO.println(status(new Invalid(List.of("email"))));
}
// Output:
// 200
// 204
// 404
// 400

Discussion

  • Be the first to comment on this lesson.