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。