JEP 361:Switch 表达式(标准)

官方原文(英文)地址: https://openjdk.java.net/jeps/361
个人原创翻译,转载请注明出处。

Summary

Extend switch so it can be used as either a statement or an expression, and so that both forms can use either traditional case ... : labels (with fall through) or new case ... -> labels (with no fall through), with a further new statement for yielding a value from a switch expression. These changes will simplify everyday coding, and prepare the way for the use of pattern matching in switch. This was a preview language feature in JDK 12 and JDK 13.

摘要

扩展switch以使其既可以用作语句,也可以用作表达式,这样可以使用两种形式:传统的case ... :标签(带有贯穿)或者新的case ... ->标签(不带有贯穿),带有另一个用于从switch表达式产生值的新语句。这些变动会简化日常编码,并为switch中的模式匹配铺平道路。这在JDK 12JDK 13中是一个预览语言特性

History

Switch expressions were proposed in December 2017 by JEP 325. JEP 325 was targeted to JDK 12 in August 2018 as a preview feature. One aspect of JEP 325 was the overloading of the break statement to return a result value from a switch expression. Feedback on JDK 12 suggested that this use of break was confusing. In response to the feedback, JEP 354 was created as an evolution of JEP 325. JEP 354 proposed a new statement, yield, and restored the original meaning of break. JEP 354 was targeted to JDK 13 in June 2019 as a preview feature. Feedback on JDK 13 suggested that switch expressions were ready to become final and permanent in JDK 14 with no further changes.

历史

JEP 3252017年12月提出了switch表达式。JEP 325在2018年8月作为JDK 12预览特性。JEP 325的一个方面是重载break语句以从switch表达式中返回结果。JDK 12的反馈表明,这样使用break会造成混淆。作为对反馈的回应,JEP 354作为JEP 325的演进而被创建。JEP 354提出了一个新的语句,yield,并恢复了break的原始含义。JEP 354在2019年6月作为JDK 13的进一步预览特性。JDK 13的反馈表明,该特性现在可以在JDK 14中最终确定并永久化。

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 default control flow behavior between switch labels (fall through), the default scoping in switch blocks (the whole block is treated as one scope), and the fact that switch works only as a statement, even though it is often more natural to express multi-way conditionals as expressions.

动机

当我们准备增强Java编程语言以支持模式匹配(JEP 305)时,现有switch语句的一些不规则性(长期以来一直困扰着用户)成为了障碍。这些包括switch标签之间的默认控制流行为(贯穿),switch块中的默认作用域(将整个块视为一个作用域),以及switch仅作为语句工作的事实,即使它通常更自然地表示为多路条件表达式。

The current design of Java's switch statement follows closely languages such as C and C++, and supports fall through semantics by default. Whilst this traditional control flow is often useful for writing low-level code (such as parsers for binary encodings), as switch is used in higher-level contexts, its error-prone nature starts to outweigh its flexibility. For example, in the following code, the many break statements make it unnecessarily verbose, and this visual noise often masks hard to debug errors, where missing break statements would mean accidental fall through.

Java中switch语句的当前设计紧密遵循C和C++等语言,默认支持贯穿语义。尽管这种传统的控制流对于编写低级代码(如用于二进制编码的解析器)很有用,但由于switch被用于更高级的上下文,其易错性超过了灵活性。例如,在下面的代码中,许多break语句使其不必要地冗长,且这种视觉噪声通常掩盖了难以调试的错误,遗漏的break语句将意味着意外的贯穿。

switch (day) {
    case MONDAY:
    case FRIDAY:
    case SUNDAY:
        System.out.println(6);
        break;
    case TUESDAY:
        System.out.println(7);
        break;
    case THURSDAY:
    case SATURDAY:
        System.out.println(8);
        break;
    case WEDNESDAY:
        System.out.println(9);
        break;
}
switch (day) {
    case MONDAY:
    case FRIDAY:
    case SUNDAY:
        System.out.println(6);
        break;
    case TUESDAY:
        System.out.println(7);
        break;
    case THURSDAY:
    case SATURDAY:
        System.out.println(8);
        break;
    case WEDNESDAY:
        System.out.println(9);
        break;
}

We propose to introduce a new form of switch label, "case L ->", to signify that only the code to the right of the label is to be executed if the label is matched. We also propose to allow multiple constants per case, separated by commas. The previous code can now be written:

我们建议引入一种新形式的switch标签,"case L ->",以表示如果匹配标签,则只执行标签右边的代码。我们还建议在每种情况下使用逗号分隔多个常量。之前的代码现在可以写成:

switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> System.out.println(6);
    case TUESDAY                -> System.out.println(7);
    case THURSDAY, SATURDAY     -> System.out.println(8);
    case WEDNESDAY              -> System.out.println(9);
}
switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> System.out.println(6);
    case TUESDAY                -> System.out.println(7);
    case THURSDAY, SATURDAY     -> System.out.println(8);
    case WEDNESDAY              -> System.out.println(9);
}

The code to the right of a "case L ->" switch label is restricted to be an expression, a block, or (for convenience) a throw statement. This has the pleasing consequence that should an arm introduce a local variable, it must be contained in a block and is thus not in scope for any of the other arms in the switch block. This eliminates another annoyance with traditional switch blocks where the scope of a local variable is the entire block:

在switch标签"case L ->"右侧的代码被限制为表达式、代码块或(为方便起见)throw语句。这有令人愉快的结果,如果一个分支引入局部变量,那它必须包含在一个代码块中,这样它就不属于switch其他分支的代码块。这消除了传统switch代码块的另一个麻烦,其中局部变量的作用域是整个代码块:

switch (day) {
    case MONDAY:
    case TUESDAY:
        int temp = ...     // The scope of 'temp' continues to the }
        break;
    case WEDNESDAY:
    case THURSDAY:
        int temp2 = ...    // Can't call this variable 'temp'
        break;
    default:
        int temp3 = ...    // Can't call this variable 'temp'
}
switch (day) {
    case MONDAY:
    case TUESDAY:
        int temp = ...     // 'temp' 的作用域直到 }
        break;
    case WEDNESDAY:
    case THURSDAY:
        int temp2 = ...    // 不能称该变量为 'temp'
        break;
    default:
        int temp3 = ...    // 不能称该变量为 'temp'
}

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

许多现有的switch语句其实是对switch表达式的模拟,其中每个分支要么给一个共同的目标变量赋值,要么返回一个值:

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);
}
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, using a switch expression, which is both clearer and safer:

用语句来表达这些是迂回、重复且易错的。作者本意是我们应该为每一天计算一个numLetters的值。应该直接使用switch表达式说出来,这样既更清晰又更安全:

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

反过来,扩展switch以支持表达式会带来一些额外需求,例如扩展流程分析(表达式必须始终计算一个值或意外中断),并允许某些switch表达式的case分支抛出一个异常而不是产生一个值。

Description

Arrow labels

In addition to traditional "case L :" labels in a switch block, we define a new simplified form, with "case L ->" labels. If a label is matched, then only the expression or statement to the right of the arrow is executed; there is no fall through. For example, given the following switch statement that uses the new form of labels:

描述

箭头标签

除了switch代码块中传统的"case L :"标签外,我们还定义了一种新的简化形式,"case L ->"标签。如果标签匹配,则仅执行箭头右侧的表达式或语句;没有贯穿。例如,给定以下使用新标签形式的switch语句:

static void howMany(int k) {
    switch (k) {
        case 1  -> System.out.println("one");
        case 2  -> System.out.println("two");
        default -> System.out.println("many");
    }
}
static void howMany(int k) {
    switch (k) {
        case 1  -> System.out.println("one");
        case 2  -> System.out.println("two");
        default -> System.out.println("many");
    }
}

The following code:

这些代码:

howMany(1);
howMany(2);
howMany(3);
howMany(1);
howMany(2);
howMany(3);

results in the following output:

会输出以下结果:

one
two
many
one
two
many

Switch expressions

We extend the switch statement so it can be used as an expression. For example, the previous howMany method can be rewritten to use a switch expression, so it uses only a single println.

switch表达式

我们扩展switch语句以使其可以用作表达式。例如,之前的howMany方法可以用switch表达式重写,只用一个println

static void howMany(int k) {
    System.out.println(
        switch (k) {
            case  1 -> "one";
            case  2 -> "two";
            default -> "many";
        }
    );
}
static void howMany(int k) {
    System.out.println(
        switch (k) {
            case  1 -> "one";
            case  2 -> "two";
            default -> "many";
        }
    );
}

In the common case, a switch expression will look like:

通常情况下,switch表达式如下所示:

T result = switch (arg) {
    case L1 -> e1;
    case L2 -> e2;
    default -> e3;
};
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.

switch表达式是聚合表达式;如果已知目标类型,则该类型会压入每个分支。switch表达式的类型即其自身目标类型(如果已知);如果未知,则通过组合每个case分支的类型来计算一个独立类型。

Yielding a value

Most switch expressions will have a single expression to the right of the "case L ->" switch label. In the event that a full block is needed, we introduce a new yield statement to yield a value, which becomes the value of the enclosing switch expression.

产生值

大多数switch表达式在"case L ->"标签的右侧都有一个表达式。以防需要一个完整代码块,我们引入一个新的yield语句来产生一个值,该值成为闭包switch表达式的值。

int j = switch (day) {
    case MONDAY  -> 0;
    case TUESDAY -> 1;
    default      -> {
        int k = day.toString().length();
        int result = f(k);
        yield result;
    }
};
int j = switch (day) {
    case MONDAY  -> 0;
    case TUESDAY -> 1;
    default      -> {
        int k = day.toString().length();
        int result = f(k);
        yield result;
    }
};

A switch expression can, like a switch statement, also use a traditional switch block with "case L:" switch labels (implying fall through semantics). In this case, values are yielded using the new yield statement:

switch语句一样,switch表达式也可以使用带有"case L:"标签的传统switch代码块(暗示着贯穿语义)。在这种情况下,使用新的yield语句产生值:

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

The two statements, break (with or without a label) and yield, facilitate easy disambiguation between switch statements and switch expressions: a switch statement but not a switch expression can be the target of a break statement; and a switch expression but not a switch statement can be the target of a yield statement.

两种语句,break(带有或不带有标签)和yield,有助于在switch语句和switch表达式之间轻松消除歧义:break语句的目标只能是switch语句,而不能是switch表达式;而yield语句的目标只能是switch表达式,而不能是switch语句。

Rather than being a keyword, yield is a restricted identifier (like var), which means that classes named yield are illegal. If there is a unary method yield in scope, then the expression yield(x) would be ambiguous (could be either a method call, or a yield statement whose operand is a parenthesized expression), and this ambiguity is resolved in favor of the yield statement. If the method invocation is preferred then the method should be qualified, with this for an instance method or the class name for a static method.

yield不是一个关键字,而是一个受限的标识符(就像var),这意味着命名为yield的类是非法的。如果作用域内有一元方法yield,那么表达式yield(x)会有歧义(既可以是一个方法调用,又可以是一个操作数为括号表达式的yield语句),解决这种歧义将有利于yield语句。如果首选方法调用,则应该使用this限定实例方法,或使用类名称限定静态方法。

Exhaustiveness

The cases of a switch expression must be exhaustive; for all possible values there must be a matching switch label. (Obviously switch statements are not required to be exhaustive.)

详尽

switch表达式的case必须详尽;对于所有可能的值,必须有一个匹配的switch标签。(显然switch语句并不要求详尽。)

In practice this normally means that a default clause is required; however, in the case of an enum switch expression that covers all known constants, a default clause is inserted by the compiler to indicate that the enum definition has changed between compile-time and runtime. Relying on this implicit default clause insertion makes for more robust code; now when code is recompiled, the compiler checks that all cases are explicitly handled. Had the developer inserted an explicit default clause (as is the case today) a possible error will have been hidden.

在实践中,这通常意味着需要一个default子句;然而,对于涵盖所有已知常量的enum switch表达式,default子句会由编译器插入,用来表明enum定义已在编译器和运行期之间改变。依靠插入这种隐式的default子句可以让代码更健壮。现在,当代码重新编译时,编译器会检查所有的case是否得到显式处理。如果开发人员插入了显式的default子句(如今天的情况),则可能的错误将被隐藏。

Furthermore, a switch expression must either complete normally with a value, or complete abruptly by throwing an exception. This has a number of consequences. First, the compiler checks that for every switch label, if it is matched then a value can be yielded.

此外,switch必须以一个值正常完成,或以抛出异常意外中断。这有许多后果。首先,编译器会为每个switch标签进行检查,如果匹配,则会产生一个值。

int i = switch (day) {
    case MONDAY -> {
        System.out.println("Monday"); 
        // ERROR! Block doesn't contain a yield statement
    }
    default -> 1;
};
i = switch (day) {
    case MONDAY, TUESDAY, WEDNESDAY: 
        yield 0;
    default: 
        System.out.println("Second half of the week");
        // ERROR! Group doesn't contain a yield statement
};
int i = switch (day) {
    case MONDAY -> {
        System.out.println("Monday"); 
        // 错误!代码块未包含 yield 语句
    }
    default -> 1;
};
i = switch (day) {
    case MONDAY, TUESDAY, WEDNESDAY: 
        yield 0;
    default: 
        System.out.println("Second half of the week");
        // 错误!组未包含 yield 语句
};

A further consequence is that the control statements, break, yield, return and continue, cannot jump through a switch expression, such as in the following:

进一步的后果是,控制语句(breakyieldreturncontinue)不能跳转switch表达式,如下所示:

z: 
    for (int i = 0; i < MAX_VALUE; ++i) {
        int k = switch (e) { 
            case 0:  
                yield 1;
            case 1:
                yield 2;
            default: 
                continue z; 
                // ERROR! Illegal jump through a switch expression 
        };
    ...
    }
z: 
    for (int i = 0; i < MAX_VALUE; ++i) {
        int k = switch (e) { 
            case 0:  
                yield 1;
            case 1:
                yield 2;
            default: 
                continue z; 
                // 错误!非法跳转 switch 表达式
        };
    ...
    }

Dependencies

This JEP evolved from JEP 325 and JEP 354. However, this JEP is standalone, and does not depend on those two JEPs.

依赖

这个JEP演进自JEP 325JEP 354。然而,这个JEP是独立的,并不依赖那两个JEP。

Future support for pattern matching, beginning with JEP 305, will build on this JEP.

未来对模式匹配的支持,将以JEP 305作为开始,基于这个JEP建立。

Risks and Assumptions

The need for a switch statement with case L -> labels is sometimes unclear. The following considerations supported its inclusion:

  • There are switch statements that operate by side-effects, but which are generally still "one action per label". Bringing these into the fold with new-style labels makes the statements more straightforward and less error-prone.
  • That the default control flow in a switch statement's block is to fall through, rather than to break out, was an unfortunate choice early in Java's history, and continues to be a matter of significant angst for developers. By addressing this for the switch construct in general, not just for switch expressions, the impact of this choice is reduced.
  • By teasing the desired benefits (expression-ness, better control flow, saner scoping) into orthogonal features, switch expressions and switch statements could have more in common. The greater the divergence between switch expressions and switch statements, the more complex the language is to learn, and the more sharp edges there are for developers to cut themselves on.

风险与假设

有时不清楚是否需要使用带有case L ->标签的switch语句。以下几点考虑支持将其包含在内:

  • 有的switch语句用到了副作用,但通常仍然是“每个标签一个动作”。通过使用新型标签将这些内容折叠起来,可以让语句更直接、更不易出错。
  • 在Java的早期,不幸的选择是switch语句块中的默认控制流是贯穿,而不是跳出,这对开发者来说仍是一个巨大的忧虑。为通常的switch构造解决该问题(而不是只为switch表达式)可以减少该选择的影响。
  • 将预期的收益(表达式、更好的控制流、清晰的作用域)发挥到正交特性中,switch表达式和switch语句可以有更多共同点。switch表达式和switch语句之间的差异越大,语言学习就越复杂,开发者利用它们获得的优势就越大。