JEP 325: Switch Expressions (Preview) | Switch 表达式(预览版)
摘要
扩展 switch 语句,使其既可以作为语句使用,也可以作为表达式使用,并且这两种形式都可以使用“传统”或“简化”的作用域和控制流行为。这些更改将简化日常编码,同时也为在 switch 中使用 模式匹配(JEP 305) 铺平了道路。这是 JDK 12 中的一个 预览语言特性。
请注意:此 JEP 已被 JEP 354 取代,后者针对 JDK 13。
动机
随着我们准备增强 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);
}(此示例还使用了多个 case 标签:我们提议支持在一个单独的 switch 标签中使用多个以逗号分隔的标签。)
"case L ->" switch 标签右侧的代码限制为表达式、代码块或(为了方便起见)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;
default:
throw new IllegalStateException("Invalid day: " + day);
};反过来,将 switch 扩展到支持表达式会引发一些额外的需求,例如扩展流分析(表达式必须始终计算一个值或突然完成),并允许 switch 表达式的某些 case 分支抛出异常而不是返回值。
描述
除了“传统”的 switch 块外,我们提议添加一种新的“简化”形式,使用新的 "case L ->" switch 标签。如果某个标签匹配,则仅执行箭头标签右侧的表达式或语句;不存在穿透现象。例如,给定以下方法:
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;
};在这个表达式中,arg 是要匹配的参数,L1、L2 等是可能的标签,e1、e2 等是当相应标签匹配时要执行的表达式,T 是表达式的返回类型,而 e3 是当没有标签匹配时执行的默认表达式。每个 case 分支的结果必须是相同的类型或可隐式转换为相同的类型,以便可以将其分配给 result 变量。
注意
由于 switch 现在可以作为表达式使用,因此它的行为将更接近于函数式编程中的模式匹配。这不仅可以提高代码的可读性和简洁性,还可以减少错误,因为不再需要 break 语句来防止穿透。
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("既不是 Foo 也不是 Bar,嗯...");
break 0;
};break 的两种形式(带值和不带值)与方法中 return 的两种形式类似。两种形式的 return 都会立即终止方法的执行;对于非 void 方法,还需要提供一个值,该值将返回给方法的调用者。(break 表达式值和 break 标签形式之间的歧义可以相对容易地处理。)
switch 表达式的情况必须是穷举的;对于任何可能的值,都必须有一个匹配的 switch 标签。在实践中,这通常意味着需要一个 default 子句;然而,在覆盖了所有已知情况的 enum switch 表达式(以及最终,在密封类型上的 switch 表达式)的情况下,编译器可以插入一个 default 子句,表明 enum 定义在编译时和运行时之间已经发生了更改。(这是开发者今天手动做的事情,但由编译器插入既不那么侵入,又可能比手动编写的错误消息更具描述性。)
此外,switch 表达式必须正常完成并返回一个值,或者抛出一个异常。这带来了一些后果。首先,编译器会检查每个 switch 标签,如果匹配,则必须能够产生一个值。
int i = switch (day) {
case MONDAY -> {
System.out.println("Monday");
// 错误!块中不包含带值的 break
}
default -> 1;
};
i = switch (day) {
case MONDAY, TUESDAY, WEDNESDAY:
break 0; // 错误!在这里,应该使用箭头 (->) 而不是 break
default:
System.out.println("Second half of the week");
// 错误!组中没有包含带值的 break(或箭头 (->) 表达式)
};注意
在第二个例子中,对于多个 case 标签的组合,应该使用箭头 (->) 来指定一个值,而不是 break。break 在这里是不正确的,因为它不是用来在 switch 表达式中提供值的。
进一步的结果是,控制语句 break、return 和 continue 不能通过 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; // 错误!不能通过 switch 表达式进行非法跳转
};
...
}作为机会目标,我们可能会扩展 switch 以支持之前不允许的基本类型(以及它们的包装类型),例如 float、double 和 long。
依赖项
模式匹配(JEP 305) 依赖于这个 JEP。