扩展switch以便它可以用作语句或表达式,并且这两种形式都可以使用传统case … :标签(有落差)或新的case … -> 标签(没有落差)。这种新的语句用于从switch生成一个值。这些更改将简化日常编码,并为日后模式匹配做准备. 这是一个预览语言特性的JDK 12和JDK 13。
Switch 表达式于 2017 年 12 月由JEP 325 提出。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 标签之间的默认控制流行为(fall
through),switch 块中的默认范围(整个块被视为一个范围),以及switch仅作为语句工作的事实,即使它通常更自然将多路条件表示为表达式。
Java switch语句的当前设计紧跟 C 和 C++ 等语言,并且默认支持 fall through
语义。虽然这种传统的控制流对于编写低级代码(例如二进制编码的解析器)通常很有用,正如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 ->" switch标签右侧的代码被限制为表达式、块或(为方便起见)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'
}
许多现有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 ->”标签。如果匹配到一个标签,则只执行箭头右侧的表达式或语句;没有失败。例如,给定以下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语句,使其可以用作表达式。例如,前面的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 ->" switch标签的右侧都有一个表达式。如果需要一个完整的块,我们会引入一个新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语句一样,也使用传统的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语句而不是switch表达式可以成为break语句的目标;并且switch表达式但不是switch语句可以是yield语句的目标。
yield不是关键字,而是受限标识符(如var),这意味着命名yield的类是非法的。如果yield作用域中有一个一元方法,那么表达式yield(x)将是不明确的(可以是方法调用,也可以是其操作数是带括号的表达式的 yield
语句),并且这种歧义将得到有利于 yield 语句的解决。如果首选方法调用,则应限定该方法,this对于实例方法或静态方法的类名称。
switch表达式的情况必须是详尽无遗的;对于所有可能的值,必须有一个匹配的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表达式,如在下文中:
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 从JEP 325和JEP 354演变而来。但是,此 JEP 是独立的,不依赖于这两个 JEP。
从JEP 305开始,未来对模式匹配的支持将建立在这个 JEP 之上。
有时不清楚是否需要switch带有case L ->标签的声明。以下考虑支持将其纳入: