跳到主要内容

JEP 361:切换表达式

概括

扩展switch它,使其可以用作语句或表达式,并且两种形式都可以使用传统case ... :标签(有失败)或新case ... ->标签(没有失败),还有一个新语句用于从switch表达。这些更改将简化日常编码,并为在switch.这是JDK 12JDK 13中的预览语言功能

历史

Switch 表达式由JEP 325于2017 年 12 月提出。 JEP 325于 2018 年 8 月作为预览功能针对 JDK 12。 JEP 325 的一方面是重载语句以从 switch 表达式返回结果值。 JDK 12 的反馈表明,这种用法令人困惑。为了响应反馈,JEP 354作为 JEP 325 的演变而创建。JEP 354 提出了新的表述 ,并恢复了 的原始含义。 JEP 354于 2019 年 6 月作为预览功能针对 JDK 13 。 JDK 13 的反馈表明 switch 表达式已准备好在 JDK 14 中成为最终且永久的,无需进一步更改。break``break``yield``break

动机

当我们准备增强 Java 编程语言以支持模式匹配 (JEP 305)时,现有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;
}

我们建议引入一种新形式的开关标签“ case L ->”,表示如果标签匹配,则仅执行标签右侧的代码。我们还建议每种情况允许使用多个常量,并用逗号分隔。之前的代码现在可以写成:

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 ->仅限于表达式、块或(为了方便起见)throw语句。这具有令人愉快的结果,如果一个臂引入局部变量,它必须包含在一个块中,因此不在开关块中任何其他臂的范围内。这消除了传统 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到支持表达式会引发一些额外的需求,例如扩展流分析(表达式必须始终计算值或突然完成),并允许表达式的某些 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语句,使其可以用作表达式。例如,前面的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是其目标类型(如果已知);如果不是,则通过组合每个案例臂的类型来计算独立类型。

产生一个值

大多数switch表达式在“”开关标签右侧都有一个表达式case L ->。如果需要完整的块,我们引入一个新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使用带有“”开关标签的传统开关块case L:(意味着落入语义)。在这种情况下,使用新语句生成值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陈述不需要详尽无遗。)

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

此外,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
};

进一步的结果是控制语句 、breakyieldreturn不能continue跳过switch表达式,如下所示:

z: 
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 325JEP 354演变而来的。但是,此 JEP 是独立的,不依赖于这两个 JEP。

JEP 305开始,未来对模式匹配的支持将在此 JEP 的基础上构建。

风险和假设

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

  • 有些switch语句会产生副作用,但通常仍然是“每个标签一个操作”。将这些内容与新式标签结合起来使语句更加简单且不易出错。

  • 语句块中的默认控制流switch是失败而不是突破,这在 Java 历史的早期是一个不幸的选择,并且仍然是开发人员的一个重大焦虑问题。通过针对switch一般构造(而不仅仅是switch表达式)解决这个问题,可以减少此选择的影响。

  • 通过将所需的好处(表达性、更好的控制流程、更合理的范围)融入正交特征中,switch表达式和switch语句可以有更多的共同点。switch表达式和语句之间的差异越大switch,语言学习起来就越复杂,开发人员就越容易被割伤。