当前位置: 首页 > 工具软件 > Switch > 使用案例 >

Java17 switch表达式

郎项禹
2023-12-01

概括

扩展switch以便它可以用作语句或表达式,并且这两种形式都可以使用传统case … :标签(有落差)或新的case … -> 标签(没有落差)。这种新的语句用于从switch生成一个值。这些更改将简化日常编码,并为日后模式匹配做准备. 这是一个预览语言特性的JDK 12和JDK 13。

历史

Switch 表达式于 2017 年 12 月由JEP 325 提出。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 标签之间的默认控制流行为(fall
through),switch 块中的默认范围(整个块被视为一个范围),以及switch仅作为语句工作的事实,即使它通常更自然将多路条件表示为表达式。

Java switch语句的当前设计紧跟 C 和 C++ 等语言,并且默认支持 fall through
语义。虽然这种传统的控制流对于编写低级代码(例如二进制编码的解析器)通常很有用,正如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标签右侧的代码被限制为表达式、块或(为方便起见)throw语句。这有一个令人愉快的结果,如果一个分支引入一个局部变量,却不在 switch 块中任何其他分支的范围内。这消除了传统 switch 块的另一个烦恼,其中局部变量的范围是整个块:

switch (day) {
    case MONDAY:
    case TUESDAY:
        int temp = ...     // The scope of 'temp' continues to the }
        break;
    case WEDNESDAY:
    case THURSDAY:
        int temp2 = ...    // Can't call this variable 'temp'
        break;
    default:
        int temp3 = ...    // Can't call this variable '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表达式的某些 case 分支抛出异常而不是生成值。

描述

箭头标签

除了switch块中传统的“ case L :”标签外,我们还定义了一种新的简化形式,带有“ case L ->”标签。如果匹配到一个标签,则只执行箭头右侧的表达式或语句;没有失败。例如,给定以下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
many

switch表达式

我们扩展了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表达式的类型是其目标类型(如果已知);如果不是,则通过组合每个 case 分支的类型来计算独立类型。

产生一个值

大多数switch表达式在 " case L ->" switch标签的右侧都有一个表达式。如果需要一个完整的块,我们会引入一个新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语句一样,也使用传统的switch块以“ case L:”switch的标签(通过语义意味着下降)。在这种情况下,值是使用 新的yield语句产生的:

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标签。(显然,switch语句不需要详尽无遗。)

在实践中,这通常意味着需要一个default条款;但是,在enum
switch表达式涵盖所有已知常量的情况下,default编译器会插入一个子句以指示enum定义在编译时和运行时之间发生了变化。依靠这种隐式default子句插入可以使代码更加健壮;现在,当重新编​​译代码时,编译器会检查是否已显式处理所有情况。如果开发人员插入了一个明确的default子句(就像今天的情况),一个可能的错误将被隐藏。

此外,switch表达式必须要么用一个值正常完成,要么通过抛出异常突然完成。这有很多后果。首先,编译器检查每个switch标签,如果匹配,则可以产生一个值。

int i = switch (day) {
    case MONDAY -> {
        System.out.println("Monday"); 
        // ERROR! Block doesn't contain a yield statement
    }
    default -> 1;
};
i = switch (day) {
    case MONDAY, TUESDAY, WEDNESDAY: 
        yield 0;
    default: 
        System.out.println("Second half of the week");
        // ERROR! Group doesn't contain a yield statement
};

另一种后果是,控制语句,break,yield,return和continue,无法通过跳switch表达式,如在下文中:

for (int i = 0; i < MAX_VALUE; ++i) {
    int k = switch (e) { 
        case 0:  
            yield 1;
        case 1:
            yield 2;
        default: 
            continue z; 
            // ERROR! Illegal jump through a switch expression 
    };
...
}

依赖关系

该 JEP 从JEP 325和JEP 354演变而来。但是,此 JEP 是独立的,不依赖于这两个 JEP。

从JEP 305开始,未来对模式匹配的支持将建立在这个 JEP 之上。

风险和假设

有时不清楚是否需要switch带有case L ->标签的声明。以下考虑支持将其纳入:

  • 有些switch语句会产生副作用,但通常仍然是“每个标签一个动作”。使用新式标签将这些放入折叠中,可以使语句更直接且不易出错。
  • switch语句块中的默认控制流是fall through而不是break out,在 Java 历史的早期是一个不幸的选择,并且仍然是开发人员非常焦虑的问题。通过针对switch一般构造(而不仅仅是针对switch表达式)解决此问题,可以减少此选择的影响。
  • 通过将所需的好处(表达能力、更好的控制流、更合理的范围)梳理成正交特征,switch表达式和switch语句可以有更多的共同点。switch表达式和switch语句之间的差异越大,学习的语言就越复杂,开发人员的切入点也就越锋利。
 类似资料: