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语句之间的差异越大,语言学习就越复杂,开发者利用它们获得的优势就越大。