JEP 325: Switch Expressions

AuthorBrian Goetz
OwnerJan Lahoda
Created2017/12/04 08:56
Updated2018/03/13 17:57
TypeFeature
StatusCandidate
Componentspecification / language
ScopeSE
Discussionamber dash dev at openjdk dot java dot net
EffortM
DurationM
Priority3
Reviewed byAlex Buckley
Issue8192963

Summary

Extend the switch statement so that it can be used as either a statement or an expression, and improve how switch handles nulls. These changes will simplify everyday coding, and also prepare the way for the use of pattern matching (JEP 305) in switch.

Motivation

As we prepare to enhance the Java programming language to support pattern matching (JEP 305), several irregularities of the existing switch statement -- which have long been an irritation to users -- become impediments. These include the handling of nulls (a switch statement throws NullPointerException if its argument is null) and that switch works only as a statement, but it is frequently more natural to express multi-way conditionals as expressions.

Many existing switch statements are essentially simulations of switch expressions, where each arm either assigns to a common target variable or returns a value:

int numLetters;
switch (day) {
    case MONDAY:
    case FRIDAY:
    case SUNDAY:
        numLetters = 6;
        break;
    case TUESDAY:
        numLetters = 7;
        break;
    case THURSDAY:
    case SATURDAY:
        numLetters = 8;
        break;
    case WEDNESDAY:
        numLetters = 9;
        break;
    default:
        throw new IllegalStateException("Wat: " + day);
};

Expressing this as a statement is roundabout, repetitive, and error-prone. The author meant to express that we should compute a value of numLetters for each day. It should be possible to say that directly, which is clearer and safer:

int numLetters = switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> 6;
    case TUESDAY -> 7;
    case THURSDAY, SATURDAY -> 8;
    case WEDNESDAY -> 9;
};

In turn, extending switch to support expressions raises some additional needs, such as handling a simple form of OR patterns (as illustrated above), extending flow analysis (an expression must always compute a value or complete abruptly), and allowing some case arms of a switch expression to throw an exception rather than yield a value.

Description

We will extend the switch statement so that it can additionally be used as an expression. In the common case, a switch expression will look like:

T result = switch (arg) {
    case L1 -> e1;
    case L2 -> e2;
    default -> e3;
}

A switch expression is a poly expression; if the target type is known, this type is pushed down into each arm. The type of a switch expression is its target type, if known; if not, a standalone type is computed by combining the types of each case arm.

In a switch statement, the break statement terminates execution of the switch. We extend the break statement so that, for switch expressions, break takes an argument, which becomes the value of the switch expression.

Case arms of switch expressions can execute statements in addition to yielding an expression value via break, just as cases of switch statements can execute multiple statements.

int result = switch (s) {
    case "Foo":
        break 1;
    case "Bar":
        break 2;
    default:
        System.out.println("Neither Foo nor Bar, hmmm...");
        break 3;
}

However, we expect it to be a very common case for case arms to evaluate to a single expression with no statements, and we provide syntactic sugar for this situation. In a switch expression, we define

case LABEL -> expression;

to be sugar for

case LABEL: break expression;

so the above can be written more concisely as:

int result = switch (s) {
    case "Foo" -> 1;
    case "Bar" -> 2;
    default:
        System.out.println("Neither Foo nor Bar, hmmm...");
        break 3;
}

In a switch expression, we also define:

case LABEL -> throw e;

to be sugar for

case LABEL: throw e;

as in:

int result = switch (s) {
    case "Foo" -> 1;
    case "Bar" -> 2;
    default -> throw new IllegalStateException(s);
}

These forms can be mixed and matched as needed, so our switch expression above can be written as:

int result = switch (s) {
    case "Foo" -> 1;
    case "Bar" -> 2;
    default:
        System.out.println("Neither Foo nor Bar, hmmm...");
        break 3;
}

The two forms of break (with and without value) are analogous to the two forms of return in methods. Both terminate the execution of the method immediately; in a non-void method, additionally a value must be provided which is yielded to the invoker of the method. (Ambiguities between the break expression-value and break label forms can be handled relatively easily.)

The cases of a switch expression must be exhaustive; for any possible input, exactly one of the arms must be evaluated. This generally means that a default clause is required, however, in the case of an enum switch expression that covers all known cases (and eventually, switch expressions over sealed types), a default clause can be inserted by the compiler that indicates that the enum definition has changed between compile time and runtime. (This is what developers do by hand today, but having the compiler insert it is both less intrusive and likely to have a more descriptive error message than the ones written by hand.)

We further make some enhancements to switch in general, which are applicable equally to statement and expression switch. A case clause can provide a comma-separated list of alternatives, rather than just one, such as:

switch (day) {
    case MONDAY, FRIDAY, SUNDAY: 
        numLetters = 6;
        break;
    ...
};

In a switch whose whose argument is a reference type (which currently is limited to boxed primitives, strings, and enums), a case clause can can explicitly specify null:

String formatted = switch (s) {
    case null -> "(null)";
    case "" -> "(empty)";
    default -> s;
}

If a switch does not include case null clause, then it is interpreted as if the first case clause was:

case null: throw new NullPointerException();

which is consistent with the current behavior of switch on reference types.

As a target of opportunity, we may expand switch to support switching on primitive types (and their box types) that have previously been disallowed, such as float, double, and long.

Dependencies

Pattern Matching (JEP 305) depends on this JEP.