跳到主要内容

JEP 325:Switch 表达式(预览)

QWen Max 中英对照 JEP 325 Switch Expressions (Preview)

概述

扩展 switch 语句,使其既可以用作语句,也可以用作表达式,并且两种形式都可以使用“传统”或“简化”的作用域和控制流行为。这些更改将简化日常编码,同时也为在 switch 中使用模式匹配(JEP 305)铺平了道路。这是 JDK 12 中的预览语言功能

请注意:此 JEP 已被 JEP 354 取代,后者针对 JDK 13。

动机

当我们准备增强 Java 编程语言以支持模式匹配 (JEP 305)时,现有的 switch 语句的一些不规则性(长期以来一直令用户感到烦恼)成为了阻碍。这些问题包括:switch 块的默认控制流行为(贯穿)、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 ->”,表示如果标签匹配,只执行标签右侧的代码。例如,之前的代码现在可以写成:

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 标签:我们建议支持在单个 switch 标签中使用多个逗号分隔的标签。)

在 "case L ->" 开关标签右侧的代码被限制为表达式、代码块,或者(为了方便起见)throw 语句。这样做的一个令人满意的结果是,如果某个分支引入了局部变量,则该变量必须包含在代码块中,因此不在 switch 代码块中其他任何分支的作用范围内。这消除了“传统” switch 代码块中的另一个烦恼,即局部变量的作用范围是整个 switch 代码块。

switch (day) {
case MONDAY:
case TUESDAY:
int temp = ...
break;
case WEDNESDAY:
case THURSDAY:
int temp2 = ... // Why can't I call this temp?
break;
default:
int temp3 = ... // Why can't I call this 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 表达式的某些分支抛出异常而不是产生一个值。

描述

除了“传统”的 switch 代码块之外,我们建议新增一种“简化”形式,并引入新的“case L ->switch 标签。如果某个标签匹配成功,则仅执行箭头标签右侧的表达式或语句;不会贯穿执行(fall through)。例如,给定以下方法:

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

以下代码:

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

结果输出如下:

one
two
many

我们将扩展 switch 语句,使其还可以用作表达式。在常见情况下,switch 表达式将如下所示:

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

switch 表达式是一种多态表达式;如果目标类型已知,该类型会被推送到每个分支中。switch 表达式的类型是其目标类型(如果已知);如果未知,则通过合并每个 case 分支的类型来计算出一个独立类型。

大多数 switch 表达式在 "case L ->" 标签的右侧会有一个表达式。如果需要一个完整的代码块,我们扩展了 break 语句以接受一个参数,该参数将成为外围 switch 表达式的值。

int j = switch (day) {
case MONDAY -> 0;
case TUESDAY -> 1;
default -> {
int k = day.toString().length();
int result = f(k);
break result;
}
};

switch 表达式也可以像 switch 语句一样,使用带有“case L:”标签的“传统” switch 块(意味着贯穿语义)。在这种情况下,可以使用带值的 break 语句来生成值:

int result = switch (s) {
case "Foo":
break 1;
case "Bar":
break 2;
default:
System.out.println("Neither Foo nor Bar, hmmm...");
break 0;
};

break 的两种形式(带值和不带值)类似于方法中的两种 return 形式。两种 return 形式都会立即终止方法的执行;在非空方法中,还必须提供一个值,该值将返回给方法的调用者。(break 表达式值和 break 标签形式之间的歧义可以相对容易地处理。)

switch 表达式的分支必须是穷尽的;对于任何可能的值,都必须有一个匹配的开关标签。在实际应用中,这通常意味着需要一个 default 子句;然而,在 enum 类型的 switch 表达式覆盖所有已知情况的情况下(最终包括对密封类型进行的 switch 表达式),编译器可以插入一个 default 子句,表明 enum 定义在编译时和运行时之间发生了变化。(这正是开发者目前手动完成的工作,但让编译器自动插入不仅侵入性更小,而且相比手动编写的错误信息,可能会提供更具描述性的错误消息。)

此外,switch 表达式必须以一个值正常完成,或者抛出一个异常。这带来了一些后果。首先,编译器会检查每个 switch 标签,如果匹配成功,则必须能够生成一个值。

int i = switch (day) {
case MONDAY -> {
System.out.println("Monday");
// ERROR! Block doesn't contain a break with value
}
default -> 1;
};
i = switch (day) {
case MONDAY, TUESDAY, WEDNESDAY:
break 0;
default:
System.out.println("Second half of the week");
// ERROR! Group doesn't contain a break with value
};

另一个结果是,控制语句 breakreturncontinue 不能跳转通过一个 switch 表达式,例如以下情况:

z: 
for (int i = 0; i < MAX_VALUE; ++i) {
int k = switch (e) {
case 0:
break 1;
case 1:
break 2;
default:
continue z;
// ERROR! Illegal jump through a switch expression
};
...
}

作为一个机会目标,我们可以扩展 switch 以支持之前被禁止的原始类型(及其包装类型)的切换,例如 float、double 和 long。

依赖

模式匹配 (JEP 305) 依赖于本 JEP。