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标签,“case L ->
”,以表示如果匹配标签,则只执行标签右边的代码。我们还建议在每种情况下使用逗号分隔多个常量。之前的代码现在可以写成:
在switch标签”case L ->
”右侧的代码被限制为表达式、代码块或(为方便起见)throw
语句。这有令人愉快的结果,如果一个分支引入局部变量,那它必须包含在一个代码块中,这样它就不属于switch其他分支的代码块。这消除了传统switch代码块的另一个麻烦,其中局部变量的作用域是整个代码块:
许多现有的switch
语句其实是对switch
表达式的模拟,其中每个分支要么给一个共同的目标变量赋值,要么返回一个值:
用语句来表达这些是迂回、重复且易错的。作者本意是我们应该为每一天计算一个numLetters
的值。应该直接使用switch
表达式说出来,这样既更清晰又更安全:
反过来,扩展switch
以支持表达式会带来一些额外需求,例如扩展流程分析(表达式必须始终计算一个值或意外中断),并允许某些switch
表达式的case
分支抛出一个异常而不是产生一个值。
描述
箭头标签
除了switch
代码块中传统的”case L :
”标签外,我们还定义了一种新的简化形式,“case L ->
”标签。如果标签匹配,则仅执行箭头右侧的表达式或语句;没有贯穿。例如,给定以下使用新标签形式的switch
语句:
这些代码:
会输出以下结果:
switch表达式
我们扩展switch
语句以使其可以用作表达式。例如,之前的howMany
方法可以用switch
表达式重写,只用一个println
。
通常情况下,switch
表达式如下所示:
switch
表达式是聚合表达式;如果已知目标类型,则该类型会压入每个分支。switch
表达式的类型即其自身目标类型(如果已知);如果未知,则通过组合每个case
分支的类型来计算一个独立类型。
产生值
大多数switch
表达式在”case L ->
”标签的右侧都有一个表达式。以防需要一个完整代码块,我们引入一个新的yield
语句来产生一个值,该值成为闭包switch
表达式的值。
像switch
语句一样,switch
表达式也可以使用带有”case L:
”标签的传统switch
代码块(暗示着贯穿语义)。在这种情况下,使用新的yield
语句产生值:
两种语句,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
标签进行检查,如果匹配,则会产生一个值。
进一步的后果是,控制语句(break
、yield
、return
和continue
)不能跳转switch
表达式,如下所示:
依赖
这个JEP演进自JEP 325和JEP 354。然而,这个JEP是独立的,并不依赖那两个JEP。
未来对模式匹配的支持,将以JEP 305作为开始,基于这个JEP建立。
风险与假设
有时不清楚是否需要使用带有case L ->
标签的switch
语句。以下几点考虑支持将其包含在内:
- 有的
switch
语句用到了副作用,但通常仍然是“每个标签一个动作”。通过使用新型标签将这些内容折叠起来,可以让语句更直接、更不易出错。 - 在Java的早期,不幸的选择是
switch
语句块中的默认控制流是贯穿,而不是跳出,这对开发者来说仍是一个巨大的忧虑。为通常的switch
构造解决该问题(而不是只为switch
表达式)可以减少该选择的影响。 - 将预期的收益(表达式、更好的控制流、清晰的作用域)发挥到正交特性中,
switch
表达式和switch
语句可以有更多共同点。switch
表达式和switch
语句之间的差异越大,语言学习就越复杂,开发者利用它们获得的优势就越大。