JEP 354:Switch 表达式(第二次预览)
概述
扩展 switch
使其既可以用作语句,也可以用作表达式,并且两种形式都可以使用传统的 case ... :
标签(带贯穿)或新的 case ... ->
标签(不带贯穿),同时新增一种语句用于从 switch
表达式中生成值。这些更改将简化日常编码,并为在 switch
中使用 模式匹配 (JEP 305) 做好准备。这是 JDK 13 中的一项 预览语言功能。
历史
Switch 表达式由 JEP 325 于 2017 年 12 月提出。它们于 2018 年 8 月被定位为 JDK 12 的功能,并作为 预览功能 提出。最初,开发者们就该功能的设计寻求反馈,随后又针对使用 switch
表达式和增强型 switch
语句的体验进行了意见收集。基于这些反馈,本 JEP 对该功能做出了一项更改:
为了从
switch
表达式中生成值,带有值的break
语句被弃用,转而使用yield
语句。
动机
当我们准备增强 Java 编程语言以支持模式匹配 (JEP 305)时,现有 switch
语句的一些不规则性(长期以来一直是用户的烦恼)成为了阻碍。这些包括开关标签之间的默认控制流行为(贯穿)、开关块中的默认作用域(整个块被视为一个作用域),以及 switch
只能作为语句使用的事实,尽管通常更自然的方式是将多路条件表达为表达式。
Java 的 switch
语句的当前设计紧随 C 和 C++ 等语言,默认支持贯穿语义(fall through semantics)。虽然这种传统的控制流在编写低级代码时非常有用(例如二进制编码的解析器),但当 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 ->
”,以表示如果标签匹配,只执行标签右侧的代码。我们还建议允许每个 case 有多个常量,用逗号分隔。之前的代码现在可以写成:
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);
}
在 "case L ->
" 开关标签右侧的代码被限制为一个表达式、一个代码块,或者(为了方便起见)一个 throw
语句。这一限制带来了一个令人满意的结果:如果某个分支引入了局部变量,则该变量必须包含在一个代码块中,因此不会进入 switch 代码块中其他分支的作用域。这就消除了传统 switch 代码块中的另一个烦人之处,即局部变量的作用域是整个代码块:
// 传统 switch 示例
switch (day) {
case MONDAY:
int a = 10; // 变量 a 的作用域是整个 switch 块
System.out.println(a);
break;
case TUESDAY:
// 这里也可以访问变量 a,容易引发错误
System.out.println(a + 5);
break;
}
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
语句本质上是在模拟 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 ->
" 标签。如果某个标签匹配成功,则仅执行箭头右侧的表达式或语句;不会贯穿执行(fall through)。例如,给定以下使用新形式标签的 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);
结果输出如下:
one
two
many
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
表达式之间进行简单的消歧:switch
语句可以作为 break
语句的目标,而 switch
表达式则不可以;switch
表达式可以作为 yield
语句的目标,而 switch
语句则不可以。
在之前
switch
表达式的预览版本中,JEP 325 提议新增一种带有值的break
语句,用于从switch
表达式中返回一个值。在当前版本的switch
表达式中,这一功能将被新的yield
语句取代。
完备性
switch
表达式的 case 必须是穷尽的;对于所有可能的值,必须有一个匹配的 switch 标签。(显然,switch
语句并不要求是穷尽的。)
在实践中,这通常意味着需要一个 default
子句;然而,在 enum
的 switch
表达式覆盖所有已知常量的情况下,编译器会插入一个 default
子句,以表明 enum
定义在编译时和运行时之间发生了变化。依赖这种隐式的 default
子句插入会使代码更加健壮;现在当代码重新编译时,编译器会检查所有情况是否都得到了显式处理。如果开发者插入了一个显式的 default
子句(像当前常见的情况一样),就可能隐藏了一个潜在的错误。
此外,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
};
另一个结果是,控制语句 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;
// ERROR! Illegal jump through a switch expression
};
...
}
依赖关系
此功能在 JEP 325 中进行了预览。
模式匹配 (JEP 305) 依赖于本 JEP。
风险与假设
对于带有 case L ->
标签的 switch
语句的需求有时并不明确。以下理由阐述了其被包含的背后假设:
-
存在通过副作用操作的
switch
语句,但通常仍然是“每个标签对应一个动作”。使用新式标签将这些语句整合进来后,语句会变得更加直接且不易出错。 -
在 Java 的早期历史中,
switch
语句块中的默认控制流是贯穿执行而不是跳出,这是一个不幸的选择。这一特性让开发者感到极度困扰。这似乎是一个需要为整个switch
构造解决的问题,而不仅仅是针对switch
表达式。 -
将期望的优势(表达式特性、更好的控制流、更合理的范围界定)拆分为正交的特性似乎更为可取,这样
switch
表达式和switch
语句可以有更多共同点。switch
表达式与switch
语句之间的差异越大,语言的学习难度就越高,开发者就越容易遇到棘手的问题。