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 语句之间的差异越大,语言的学习难度就越大,开发人员遇到的棘手问题也就越多。