Skip to content

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 语句将意味着意外的穿透。

java
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 中包含多个常量,常量之间用逗号分隔。之前的代码现在可以写成:

java
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 块中的另一个烦恼,即局部变量的作用域是整个块:

java
switch (day) {
    case MONDAY:
    case TUESDAY:
        int temp = ...     // 'temp'的作用域一直持续到}
        break;
    case WEDNESDAY:
    case THURSDAY:
        int temp2 = ...    // 不能将此变量命名为'temp'
        break;
    default:
        int temp3 = ...    // 不能将此变量命名为'temp'
}

许多现有的 switch 语句本质上是对 switch 表达式的模拟,其中每个分支要么给一个公共目标变量赋值,要么返回一个值:

java
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表达式 来直接表达这一点,这样做既清晰又安全:

java
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 语句:

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

以下代码:

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

将产生以下输出:

txt
one
two
many

Switch 表达式

我们将扩展 switch 语句,使其可以用作表达式。例如,之前的 howMany 方法可以重写为使用 switch 表达式,因此它只使用单个 println

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

在一般情况下,一个 switch 表达式看起来像这样:

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

switch 表达式是一个多态表达式;如果目标类型已知,这个类型将被推送到每个分支中。switch 表达式的类型是它的目标类型(如果已知的话);如果不知道,则会通过将每个分支的类型组合在一起来计算一个独立的类型。

产生值

大多数 switch 表达式在 "case L ->" 切换标签的右侧会有一个单一的表达式。如果需要一个完整的代码块,我们引入了一个新的 yield 语句来产生一个值,这个值将成为封闭 switch 表达式的值。

java
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 语句来产生值:

java
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 表达式必须以值正常完成,或通过抛出异常突然完成。这有几个后果。首先,编译器会检查每个切换标签,如果匹配,则必须能够产生一个值。

java
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 语句
};

另一个后果是,控制语句 breakyieldreturncontinue 不能跳过一个 switch 表达式,比如下面的例子:

java
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 语句之间的差异越大,语言的学习难度就越大,开发者就越容易遇到尖锐的边缘问题。

Page Layout Max Width

Adjust the exact value of the page width of VitePress layout to adapt to different reading needs and screens.

Adjust the maximum width of the page layout
A ranged slider for user to choose and customize their desired width of the maximum width of the page layout can go.

Content Layout Max Width

Adjust the exact value of the document content width of VitePress layout to adapt to different reading needs and screens.

Adjust the maximum width of the content layout
A ranged slider for user to choose and customize their desired width of the maximum width of the content layout can go.