Skip to content

JEP 325: Switch Expressions (Preview) | Switch 表达式(预览版)

摘要

扩展 switch 语句,使其既可以作为语句使用,也可以作为表达式使用,并且这两种形式都可以使用“传统”或“简化”的作用域和控制流行为。这些更改将简化日常编码,同时也为在 switch 中使用 模式匹配(JEP 305) 铺平了道路。这是 JDK 12 中的一个 预览语言特性

请注意:此 JEP 已被 JEP 354 取代,后者针对 JDK 13。

动机

随着我们准备增强 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 ->",以表示如果标签匹配,则仅执行标签右侧的代码。例如,前面的代码现在可以写为:

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 标签:我们提议支持在一个单独的 switch 标签中使用多个以逗号分隔的标签。)

"case L ->" switch 标签右侧的代码限制为表达式、代码块或(为了方便起见)throw 语句。这带来了一个令人愉悦的结果,即如果某个分支引入了一个局部变量,那么它必须包含在一个代码块中,因此它不会对其他 switch 块中的任何分支可见。这消除了“传统” switch 块中局部变量作用域为整个 switch 块的另一个烦恼。

java
switch (day) {
    case MONDAY:
    case TUESDAY:
        int temp = ...
        break;
    case WEDNESDAY:
    case THURSDAY:
        int temp2 = ...     // Why can't I call this temp?
        break;
    default:
        int temp3 = ...     // Why can't I call this 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;
    default:
        throw new IllegalStateException("Invalid day: " + day);
};

反过来,将 switch 扩展到支持表达式会引发一些额外的需求,例如扩展流分析(表达式必须始终计算一个值或突然完成),并允许 switch 表达式的某些 case 分支抛出异常而不是返回值。

描述

除了“传统”的 switch 块外,我们提议添加一种新的“简化”形式,使用新的 "case L ->" switch 标签。如果某个标签匹配,则仅执行箭头标签右侧的表达式或语句;不存在穿透现象。例如,给定以下方法:

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

以下代码:

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

将产生以下输出:

txt
one
two
many

我们将扩展 switch 语句,以便它可以作为表达式使用。在常见情况下,switch 表达式将如下所示:

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

在这个表达式中,arg 是要匹配的参数,L1L2 等是可能的标签,e1e2 等是当相应标签匹配时要执行的表达式,T 是表达式的返回类型,而 e3 是当没有标签匹配时执行的默认表达式。每个 case 分支的结果必须是相同的类型或可隐式转换为相同的类型,以便可以将其分配给 result 变量。

注意

由于 switch 现在可以作为表达式使用,因此它的行为将更接近于函数式编程中的模式匹配。这不仅可以提高代码的可读性和简洁性,还可以减少错误,因为不再需要 break 语句来防止穿透。

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

大多数 switch 表达式将在 "case L ->" 开关标签的右侧有一个单一的表达式。如果需要一个完整的代码块,我们已经扩展了 break 语句以接受一个参数,该参数将成为包含它的 switch 表达式的值。

java
int j = switch (day) {
    case MONDAY  -> 0;
    case TUESDAY -> 1;
    default      -> {
        int k = day.toString().length();
        int result = f(k);
        break result;
    }
};

switch 表达式可以像 switch 语句一样,使用带有 "case L:" 开关标签的“传统”switch 块(意味着有穿透语义)。在这种情况下,可以使用带值的 break 语句来产生值:

java
int result = switch (s) {
    case "Foo": 
        break 1;
    case "Bar":
        break 2;
    default:
        System.out.println("既不是 Foo 也不是 Bar,嗯...");
        break 0;
};

break 的两种形式(带值和不带值)与方法中 return 的两种形式类似。两种形式的 return 都会立即终止方法的执行;对于非 void 方法,还需要提供一个值,该值将返回给方法的调用者。(break 表达式值和 break 标签形式之间的歧义可以相对容易地处理。)

switch 表达式的情况必须是穷举的;对于任何可能的值,都必须有一个匹配的 switch 标签。在实践中,这通常意味着需要一个 default 子句;然而,在覆盖了所有已知情况的 enum switch 表达式(以及最终,在密封类型上的 switch 表达式)的情况下,编译器可以插入一个 default 子句,表明 enum 定义在编译时和运行时之间已经发生了更改。(这是开发者今天手动做的事情,但由编译器插入既不那么侵入,又可能比手动编写的错误消息更具描述性。)

此外,switch 表达式必须正常完成并返回一个值,或者抛出一个异常。这带来了一些后果。首先,编译器会检查每个 switch 标签,如果匹配,则必须能够产生一个值。

java
int i = switch (day) {
    case MONDAY -> {
        System.out.println("Monday"); 
        // 错误!块中不包含带值的 break
    }
    default -> 1;
};
i = switch (day) {
    case MONDAY, TUESDAY, WEDNESDAY: 
        break 0; // 错误!在这里,应该使用箭头 (->) 而不是 break
    default: 
        System.out.println("Second half of the week");
        // 错误!组中没有包含带值的 break(或箭头 (->) 表达式)
};

注意

在第二个例子中,对于多个 case 标签的组合,应该使用箭头 (->) 来指定一个值,而不是 breakbreak 在这里是不正确的,因为它不是用来在 switch 表达式中提供值的。

进一步的结果是,控制语句 breakreturncontinue 不能通过 switch 表达式进行跳转,如下例所示:

java
z: 
    for (int i = 0; i < MAX_VALUE; ++i) {
        int k = switch (e) { 
            case 0:  
                break 1; // 错误用法
            case 1:
                break 2; // 错误用法
            default: 
                continue z; // 错误!不能通过 switch 表达式进行非法跳转
        };
    ...
    }

作为机会目标,我们可能会扩展 switch 以支持之前不允许的基本类型(以及它们的包装类型),例如 floatdoublelong

依赖项

模式匹配(JEP 305) 依赖于这个 JEP。