Skip to content

JEP 361: Switch Expressions | Switch 表达式

摘要

扩展 switch 语句,使其既可以作为语句使用,也可以作为表达式使用,并且两种形式都可以使用传统的 case ... : 标签(具有穿透性)或新的 case ... -> 标签(无穿透性),以及一个新的语句,用于从 switch 表达式中返回值。这些更改将简化日常编码,并为在 switch 中使用模式匹配铺平道路。这是 JDK 12JDK 13 中的 预览语言特性

历史

JEP 3252017 年 12 月 提出了 switch 表达式。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 标签之间的默认控制流行为(穿透)、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 标签右侧的代码仅限于一个表达式、一个块或(为了方便起见)一个 throw 语句。这样做有一个令人愉快的结果,即如果一个分支引入了局部变量,它必须包含在一个块中,因此不在 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 ->" 标签。如果某个标签匹配,则仅执行箭头右侧的表达式或语句;不会执行后续的标签。例如,给定以下使用新形式标签的 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:" 切换标签的传统切换块(暗示穿透语义)。在这种情况下,值是通过新的 yield 语句返回的:

java
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 语句不要求完备。)

在实践中,这通常意味着需要一个 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("Second half of the week");
        // 错误!代码组中不包含 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 是从 JEP 325JEP 354 演变而来的。然而,这个 JEP 是独立的,并不依赖于这两个 JEP。

未来对模式匹配的支持,从 JEP 305 开始,将基于这个 JEP 进行构建。

风险和假设

有时,对带有 case L -> 标签的 switch 语句的需求并不明确。以下考虑因素支持了其包含:

  • 有些 switch 语句通过副作用进行操作,但通常仍然是“每个标签一个动作”。将这些语句与新型标签结合起来,可以使语句更加直接且不易出错。

  • 在 Java 早期历史中,switch 语句块中的默认控制流是穿透而非跳出,这是一个不幸的选择,并且仍然是开发人员关注的一个重大问题。通过针对一般的 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.