跳到主要内容

JEP 361: Switch 表达式

QWen Max 中英对照 JEP 361: Switch Expressions

概述

扩展 switch,使其既可以用作语句,也可以用作表达式,并且两种形式都可以使用传统的 case ... : 标签(带贯穿)或新的 case ... -> 标签(不带贯穿),并且新增一种语句用于从 switch 表达式中返回值。这些更改将简化日常编码,并为在 switch 中使用模式匹配铺平道路。这是一个 预览语言特性,在 JDK 12JDK 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 的反馈表明,switch 表达式已经准备好在 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;
}
java

我们建议引入一种新的 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);
}
java

在 "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'
}
java

许多现有的 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);
}
java

这样表述既迂回又重复,而且容易出错。作者的意思是表达我们应该为每一天计算一个 numLetters 的值。应该可以直接使用 switch 表达式 来表述这一点,这样既更清晰又更安全:

int numLetters = switch (day) {
case MONDAY, FRIDAY, SUNDAY -> 6;
case TUESDAY -> 7;
case THURSDAY, SATURDAY -> 8;
case WEDNESDAY -> 9;
};
java

反过来,扩展 switch 以支持表达式会引发一些额外的需求,比如扩展流分析(一个表达式必须始终计算出一个值或突然完成),以及允许 switch 表达式的某些分支抛出异常而不是产生一个值。

描述

箭头标签

除了 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");
}
}
java

以下代码:

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

结果输出如下:

one
two
many
java

Switch 表达式

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

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

在一般情况下,switch 表达式如下所示:

T result = switch (arg) {
case L1 -> e1;
case L2 -> e2;
default -> e3;
};
java

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;
}
};
java

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;
};
java

两个语句 break(无论是否带有标签)和 yield,有助于在 switch 语句和 switch 表达式之间轻松消除歧义:switch 语句可以是 break 语句的目标,而 switch 表达式则不可以;switch 表达式可以是 yield 语句的目标,而 switch 语句则不可以。

yield 并不是一个关键字,而是一个受限的标识符(类似于 var),这意味着名为 yield 的类是非法的。如果在作用域中存在一个一元方法 yield,那么表达式 yield(x) 将会产生歧义(可能是方法调用,或者是一个带有括号表达式作为操作数的 yield 语句),这种歧义会优先解释为 yield 语句。如果需要优先使用方法调用,则应对方法进行限定,对于实例方法使用 this,对于静态方法使用类名。

完备性

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

在实践中,这通常意味着需要一个 default 子句;然而,在 enumswitch 表达式覆盖所有已知常量的情况下,编译器会插入一个 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
};
java

另一个结果是控制语句 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
};
...
}
java

依赖

本 JEP 源自 JEP 325JEP 354。然而,本 JEP 是独立的,不依赖于那两个 JEP。

未来对模式匹配的支持将建立在此 JEP 的基础上,从 JEP 305 开始。

风险与假设

有时,对于带有 case L -> 标签的 switch 语句的需求并不明确。以下几点考虑支持了它的加入:

  • 存在通过副作用操作的 switch 语句,但通常仍然是“每个标签一个动作”。将这些语句用新式标签整合进来会使语句更加直接且不易出错。
  • 在 Java 历史早期,switch 语句块中的默认控制流是贯穿而不是中断,这是一个不幸的选择,并且一直是开发者们非常头疼的问题。通过针对整个 switch 结构(而不仅仅是 switch 表达式)解决这个问题,可以减少该选择带来的影响。
  • 通过将期望的好处(表达性、更好的控制流、更合理的范围界定)分解为正交特性,switch 表达式和 switch 语句可以有更多共同点。switch 表达式与 switch 语句之间的差异越大,语言就越难学,开发者就越容易遇到棘手的问题。