JEP 361:Switch 表达式(标准)
原文:https://openjdk.org/jeps/361
翻译:张欢
扩展switch以使其既可以用作语句,也可以用作表达式,这样可以使用两种形式:传统的case ... :标签(带有fall-through“贯穿”)或者新的case ... ->标签(不带有贯穿),带有另一个用于从switch表达式产生值的新语句。这些变动会简化日常编码,并为switch中的模式匹配铺平道路。这在JDK 12和JDK 13中是一个预览语言特性。
JEP 325在2017年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中最终确定并永久化。
当我们准备增强Java编程语言以支持模式匹配(JEP 305)时,现有switch语句的一些不规则性(长期以来一直困扰着用户)成为了障碍。这些包括switch标签之间的默认控制流行为(贯穿),switch块中的默认作用域(将整个块视为一个作用域),以及switch仅作为语句工作的事实,即使它通常更自然地表示为多路条件表达式。
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标签,“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标签”case L ->”右侧的代码被限制为表达式、代码块或(为方便起见)throw语句。这有令人愉快的结果,如果一个分支引入局部变量,那它必须包含在一个代码块中,这样它就不属于switch其他分支的代码块。这消除了传统switch代码块的另一个麻烦,其中局部变量的作用域是整个代码块:
switch (day) {    case MONDAY:    case TUESDAY:        int temp = ...     // 'temp' 的作用域直到 }        break;    case WEDNESDAY:    case THURSDAY:        int temp2 = ...    // 不能称该变量为 'temp'        break;    default:        int temp3 = ...    // 不能称该变量为 'temp'}许多现有的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);}用语句来表达这些是迂回、重复且易错的。作者本意是我们应该为每一天计算一个numLetters的值。应该直接使用switch表达式说出来,这样既更清晰又更安全:
int numLetters = switch (day) {    case MONDAY, FRIDAY, SUNDAY -> 6;    case TUESDAY                -> 7;    case THURSDAY, SATURDAY     -> 8;    case WEDNESDAY              -> 9;};反过来,扩展switch以支持表达式会带来一些额外需求,例如扩展流程分析(表达式必须始终计算一个值或意外中断),并允许某些switch表达式的case分支抛出一个异常而不是产生一个值。
除了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");    }}这些代码:
howMany(1);howMany(2);howMany(3);会输出以下结果:
onetwomanyswitch表达式
Section titled “switch表达式”我们扩展switch语句以使其可以用作表达式。例如,之前的howMany方法可以用switch表达式重写,只用一个println。
static void howMany(int k) {    System.out.println(        switch (k) {            case  1 -> "one";            case  2 -> "two";            default -> "many";        }    );}通常情况下,switch表达式如下所示:
T result = switch (arg) {    case L1 -> e1;    case L2 -> e2;    default -> e3;};switch表达式是聚合表达式;如果已知目标类型,则该类型会压入每个分支。switch表达式的类型即其自身目标类型(如果已知);如果未知,则通过组合每个case分支的类型来计算一个独立类型。
大多数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;    }};像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;};两种语句,break(带有或不带有标签)和yield,有助于在switch语句和switch表达式之间轻松消除歧义:break语句的目标只能是switch语句,而不能是switch表达式;而yield语句的目标只能是switch表达式,而不能是switch语句。
yield不是一个关键字,而是一个受限的标识符(就像var),这意味着命名为yield的类是非法的。如果作用域内有一元方法yield,那么表达式yield(x)会有歧义(既可以是一个方法调用,又可以是一个操作数为括号表达式的yield语句),解决这种歧义将有利于yield语句。如果首选方法调用,则应该使用this限定实例方法,或使用类名称限定静态方法。
switch表达式的case必须详尽;对于所有可能的值,必须有一个匹配的switch标签。(显然switch语句并不要求详尽。)
在实践中,这通常意味着需要一个default子句;然而,对于涵盖所有已知常量的enum switch表达式,default子句会由编译器插入,用来表明enum定义已在编译器和运行期之间改变。依靠插入这种隐式的default子句可以让代码更健壮。现在,当代码重新编译时,编译器会检查所有的case是否得到显式处理。如果开发人员插入了显式的default子句(如今天的情况),则可能的错误将被隐藏。
此外,switch必须以一个值正常完成,或以抛出异常意外中断。这有许多后果。首先,编译器会为每个switch标签进行检查,如果匹配,则会产生一个值。
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 语句};进一步的后果是,控制语句(break、yield、return和continue)不能跳转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;                // 错误!非法跳转 switch 表达式        };    ...    }这个JEP演进自JEP 325和JEP 354。然而,这个JEP是独立的,并不依赖那两个JEP。
未来对模式匹配的支持,将以JEP 305作为开始,基于这个JEP建立。
有时不清楚是否需要使用带有case L ->标签的switch语句。以下几点考虑支持将其包含在内:
- 有的switch语句用到了副作用,但通常仍然是“每个标签一个动作”。通过使用新型标签将这些内容折叠起来,可以让语句更直接、更不易出错。
- 在Java的早期,不幸的选择是switch语句块中的默认控制流是贯穿,而不是跳出,这对开发者来说仍是一个巨大的忧虑。为通常的switch构造解决该问题(而不是只为switch表达式)可以减少该选择的影响。
- 将预期的收益(表达式、更好的控制流、清晰的作用域)发挥到正交特性中,switch表达式和switch语句可以有更多共同点。switch表达式和switch语句之间的差异越大,语言学习就越复杂,开发者利用它们获得的优势就越大。