JEP 354: Switch Expressions (Second Preview) | Switch 表达式(预览版)
摘要
扩展 switch 语句,使其既可以作为语句使用,也可以作为表达式使用,并且这两种形式都可以使用传统的 case ... : 标签(具有穿透效果)或新的 case ... -> 标签(没有穿透效果),同时增加了一个新的语句用于从 switch 表达式中返回值。这些改变将简化日常编码工作,并为在 switch 中使用 模式匹配(JEP 305) 铺平了道路。这是 JDK 13 中的一个 预览语言特性。
历史
switch 表达式在 2017 年 12 月提出,由 JEP 325 提出。在 2018 年 8 月 它被定为目标在 JDK 12 中作为一个 预览特性。最初,人们针对该特性的设计征求了反馈,后来又在使用 switch 表达式和增强的 switch 语句的经验上征求了反馈。基于这些反馈,本 JEP 对该特性做了一项更改:
为了从
switch表达式中返回值,我们不再使用带有值的break语句,而是采用了一个新的yield语句。
动机
随着我们准备增强 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 ->”,以表示如果标签匹配,则仅执行标签右侧的代码。我们还提议允许每个 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 标签右侧的代码被限制为表达式、块(block)或(为了方便起见)throw 语句。这带来了一个令人愉快的结果,即如果一个分支(arm)引入了局部变量,它必须包含在一个块中,因此不在 switch 块中其他任何分支的作用域内。这消除了传统 switch 块中的另一个烦恼,即局部变量的作用域是整个块:
switch (day) {
case MONDAY:
case TUESDAY:
int temp = ... // 'temp'的作用域一直持续到}
break;
case WEDNESDAY:
case THURSDAY:
int temp2 = ... // 不能将此变量命名为'temp'
break;
default:
int temp3 = ... // 不能将此变量命名为'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 :" 标签外,我们提出了一个新的简化形式,即使用 "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");
}
}以下代码:
howMany(1);
howMany(2);
howMany(3);将产生以下输出:
one
two
manySwitch 表达式
我们将扩展 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 表达式的类型是它的目标类型(如果已知的话);如果不知道,则会通过将每个分支的类型组合在一起来计算一个独立的类型。
产生值
大多数 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;
}
};switch 表达式,就像 switch 语句一样,也可以使用传统的带有 "case L:" 切换标签的 switch 块(暗示穿透语义)。在这种情况下,使用新的 yield 语句来产生值:
int result = switch (s) {
case "Foo":
yield 1;
case "Bar":
yield 2;
default:
System.out.println("既不是 Foo 也不是 Bar,嗯...");
yield 0;
};break(带或不带标签)和 yield 这两个语句有助于轻松区分 switch 语句和 switch 表达式:switch 语句可以是 break 语句的目标,但 switch 表达式不可以;switch 表达式可以是 yield 语句的目标,但 switch 语句不可以。
在
switch表达式的先前预览版本 JEP 325 中,我们提出添加一种带有值的新形式的break语句,该语句将用于从switch表达式中产生一个值。在switch表达式的这个版本中,这将被新的yield语句所替代。
穷尽性
switch 表达式的分支必须是 穷尽的;对于所有可能的值,都必须有一个匹配的切换标签。(显然,switch 语句不需要穷尽。)
在实践中,这通常意味着需要一个 default 子句;然而,在覆盖所有已知常量的 enum switch 表达式的情况下,编译器会插入一个 default 子句,以指示在编译时和运行时之间 enum 定义已经更改。依赖这种隐式的 default 子句插入可以编写出更健壮的代码;现在当代码重新编译时,编译器会检查是否所有情况都已明确处理。如果开发者插入了显式的 default 子句(就像现在的情况),则可能隐藏了一个错误。
此外,switch 表达式必须以值正常完成,或通过抛出异常突然完成。这有几个后果。首先,编译器会检查每个切换标签,如果匹配,则必须能够产生一个值。
int i = switch (day) {
case MONDAY -> {
System.out.println("Monday");
// 错误!块中不包含 yield 语句
}
default -> 1;
};
i = switch (day) {
case MONDAY, TUESDAY, WEDNESDAY:
yield 0;
default:
System.out.println("一周的后半部分");
// 错误!分组中不包含 yield 语句
};另一个后果是,控制语句 break、yield、return 和 continue 不能跳过一个 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;
// 错误!非法地跳过 switch 表达式
};
...
}依赖项
这个特性在 JEP 325 中进行了预览。
模式匹配(JEP 305) 依赖于这个 JEP。
风险与假设
有时 switch 语句使用 case L -> 标签的需求并不清晰。以下理由阐述了引入这一特性的假设:
有些
switch语句通过副作用来操作,但通常仍然是“每个标签一个动作”。将这些语句与新样式的标签结合使用可以使语句更直接、更不易出错。switch语句块中默认的控制流是穿透(fall through),而不是跳出(break out),这是 Java 早期历史中的一个不幸选择。这对开发者来说是一个巨大的痛点。这似乎是一个应该针对switch构造本身(而不仅仅是switch表达式)解决的问题。似乎更可取的是将期望的好处(表达性、更好的控制流、更合理的作用域)拆分成正交的特性,这样
switch表达式和switch语句可以有更多的共同之处。switch表达式和switch语句之间的差异越大,语言的学习难度就越大,开发者就越容易遇到尖锐的边缘问题。