一、简介
我们前面对于模式匹配做了一些描述java模式匹配。 现有 switch 语句的几个不规则性 (长期以来一直让用户感到恼火) 成为障碍。其中包括 switch 块的默认控制流行为 (落入) 、switch 块的默认范围 (块被视为一个单一范围) 以及该 switch 仅作为语句工作,即使将多路条件表示为表达式通常更自然。听起来概念还是很晦涩的。
Java 的 switch 语句的当前设计紧跟 C 和 C++ 等语言,并默认支持直通语义。虽然这种传统的控制流通常对于编写低级代码(例如二进制编码的解析器)很有用,但随着 switch 用于更高级别的上下文,其容易出错的性质开始超过其灵活性。
例如,在下面的代码中,许多 break 语句使其不必要地冗长,并且这种视觉干扰通常会掩盖难以调试的错误,其中缺少 break 语句意味着会发生意外的故障。
int score = 80;
switch (score) {
case 10:
case 20:
case 30:
case 40:
case 50:
System.out.println("不及格");
break;
case 60:
System.out.println("及格");
break;
case 70:
System.out.println("中等");
break;
case 80:
System.out.println("良好");
break;
case 90:
System.out.println("优秀");
break;
default:
System.out.println("重开");
}
我们每个人都写过这种代码,尤其是尊贵的jdk8用户,这种代码很难看,首先,大量的break充斥在代码中,而且如果不用break,他会一直往下走(这特么还是一个八股文)。
难道我就不能执行我的分支就好了,符合条件就执行,不符合就不执行这不就得了,完全符合语义。非要整一堆没用的。
二、简洁的语法
所以在新的jdk(jdk14)我们建议引入一种新形式的 switch 标签,写成 “case L ->” 来表示如果标签匹配,则只执行标签右侧的代码。例如,现在可以编写前面的代码以这样的形式:
int score = 20;
switch (score){
case 10,20,30,40,50 -> System.out.println("不及格");
case 60 -> System.out.println("及格");
case 70 -> System.out.println("中等");
case 80 -> System.out.println("良好");
case 90 -> System.out.println("优秀");
default -> System.out.println("重开");
}
这样,我们把一类case条件以逗号隔开,就不用每一个条件都写一个case了。而且我们不需要每一个分支都来一个break去结尾,我们符合哪个就执行哪个就完事了。
三、局部变量的范围
我们来看一段jdk8上的代码。
int score = 60;
switch (score) {
case 50:
String scope50 = "case 50中声明的变量";
System.out.println("不及格");
break;
case 60:
scope50 = "case 60中的修改";
System.out.println(scope50);
System.out.println("及格");
break;
default:
System.out.println("重开");
}
你猜猜会输出什么,恭喜你你猜对了。
case 60中的修改
及格
我们看到,我们在case50中声明的变量,你竟然可以在case60中修改他,也就是说一个块中的变量的作用范围可以超出他这个块。case块中的变量的作用范围是整个switch块。这样很容易出bug,其他分支修改了不属于他的块的变量,一点也不封闭。
而在新的语法中,这个操作被修改了,你的这个case块中的变量无法被其他case块引用。
四、计算?
这个标题是不是很诡异,我们想一下我们一般如何使用switch呢,我们经常是有一个全局变量,经过不同的case 赋予不同的值。一般是这样的。
int score = 80;
String result;
switch (score) {
case 10:
case 20:
case 30:
case 40:
case 50:
result = "不及格";
break;
case 60:
result = "及格";
break;
case 70:
result = "中等";
break;
case 80:
result = "良好";
break;
case 90:
result = "优秀";
break;
default:
result = "未知";
break;
}
System.out.println(result);
但是这样的代码将其表示为语句是迂回的、重复的且容易出错的。代码本身要表达的就是每一种分支是一个结果,最后返回即可,你这样是增加了复杂度。
我们在新版本中可以改为这样,switch表达式他可以作为一种计算返回值使用。
int score = 20;
String res = switch (score){
case 10,20,30,40,50 -> "不及格";
case 60 -> "及格";
case 70 -> "中等";
case 80 -> "良好";
case 90 -> "优秀";
default -> "未知";
};
System.out.println(res);
这样当然是非常充足的语义表达。而且大多数 switch 表达式在 “case 条件 ->” 开关标签的右侧都有一个表达式。如果需要一个完整的块,我们扩展了 break 关键字,让break后面可以接受一个参数,该参数成为整个 switch 表达式的返回的值。
int score = 20;
String res = switch (score){
case 10,20,30,40,50 -> "不及格";
case 60 -> "及格";
case 70 -> "中等";
case 80 -> "良好";
case 90 -> "优秀";
default -> {
break "未知";
}
};
System.out.println(res);
这个语句很清晰,我们使用switch作为一个表达式的计算,进而返回给一个值。但是我们看到在default中,我们使用了break关键字,返回了一个值。这种设计一般用在我们的箭头右边不是一个简单的表达式,而是一个代码块,用作返回。jdk一开始是准备这么设计的,但是很快就推翻了,他们放弃了berak关键字,因为社区反馈这里的break会让我们和原来的switch中的break混乱。所以他们换了一个关键字来实现这个功能,这个关键字就是yield。
于是我们上面的代码应该是。
int score = 20;
String res = switch (score){
case 10,20,30,40,50 -> "不及格";
case 60 -> "及格";
case 70 -> "中等";
case 80 -> "良好";
case 90 -> "优秀";
default -> {
yield "未知";
}
};
System.out.println(res);
五、关于分支的穷举
switch 表达式并不是要求你对于所有的条件都必须有一个匹配的 switch 标签。换句话说,对于你输入的值,他可以有不匹配的分支。我们不可能列全的很多时候。在实践中,这通常意味着需要一个 default 子句;但是,对于涵盖所有已知常量的枚举switch 表达式,编译器会插入一个 default 子句(你不写编译器会给你插入一个),以指示枚举定义在编译时和运行时之间已更改。依赖这种隐式 default 子句插入可以使代码更加健壮;现在,当重新编译代码时,编译器会检查是否显式处理了所有情况。如果开发人员插入了显式 default 子句,则可能的错误将被隐藏。换言之就是你自己写了一个default 子句,那么他最终在无法匹配之后就会走入default 子句,这样你就不知道你输入的是不是不对,是不是遗漏了哪个case没写,反正他会走入default,不会报错。这个在开发中一定要注意。
比如这样:反正我们有default兜底,你不会发现,其实你遗漏了case 100的分支,难道没人会考一百分吗。这样的代码不会报错,健壮性很强,但是会遗漏业务。
int score = 20;
String res = switch (score){
case 10,20,30,40,50 -> "不及格";
case 60 -> {
System.out.println("及格");
yield "及格";
};
case 70 -> "中等";
case 80 -> "良好";
case 90 -> "优秀";
default -> {
yield "未知";
}
};
System.out.println(res);
此外,switch 表达式必须以值正常完成也就是匹配到了或者是走入了default分支,也或者是在执行分支的时候报错了,这样也会结束这个switch表达式。
我们来看几种情况。
int score = 20;
String res = switch (score){
case 10,20,30,40,50 -> "不及格";
case 60 -> {
// 这里会报错,我们没有给表达式返回值,
System.out.println("及格");
};
case 70 -> "中等";
case 80 -> "良好";
case 90 -> "优秀";
default -> {
yield "未知";
}
};
System.out.println(res);
而进一步我们来看一些控制语句:
int score = 20;
z:
for (int i = 0; i < 10; i++) {
String res = switch (score){
case 10,20,30,40,50 -> "不及格";
case 60 -> "及格";
case 70 -> "中等";
case 80 -> "良好";
case 90 -> "优秀";
default -> {
// 这里会报错,switch表达式中一定要有返回值,而不是这种控制结束符号,他在这里不生效
continue z;
}
};
System.out.println(res);
}
注意:上面代码continue z; 或者是break z;是java中的goto语句,用在循环中,不过实际上一般不咋用。了解一下即可。
在这种语法推出之后,我们之前jdk8的那些写法当然还是可以使用的。
switch 语句块中的默认控制流是 Falling Through,而不是 Break Out,所以我们早期必须添加break才能结束分支,这在 Java 历史的早期是一个不幸的选择,所以我们推出新的这种语法来处理这些问题并且仍然是开发人员非常焦虑的问题。
六、 fall through和 break out
我们上面说java的switch设计的是 fall through是怎么回事呢。我们以go为例来说一下,因为go支持这两种模式。
package main
import (
"fmt"
)
func main() {
score := 70
switch score {
case 60:
fmt.Println("及格")
case 70:
fmt.Println("中等")
fallthrough
case 80:
fmt.Println("良好")
case 90:
fmt.Println("优秀")
}
}
这段代码输出的是
中等
良好
他匹配了70执行了输出中等,然后因为我们在后面加了fallthrough,所以他会继续穿透该case往下走,还会执行后面的case 80,在执行了80之后此时因为默认的break out就直接结束了。
switch中的break和fallthrough语句
break:可以使用在switch中,也可以使用在for循环中
强制结束case语句,从而结束switch分支
fallthrough:用于穿透switch
当switch中某个case匹配成功之后,就执行该case语句
如果遇到fallthrough,那么后面紧邻的case,无需匹配, 执行穿透执行。此外,fallthrough应该位于某个case的最后一行
所以我们看到java默认的其实是fall through,所以你要及时结束case就必须加break,就造成了我们说的充斥着break,不过我感觉二者都可以,只不过java只支持了一种,导致可选择的不多。你看人家go,就两种都有,至于哪个c++我都不想说他,c++是在c++17才支持的两种,不过java和c++本身就是一脉相承。
以上特性在jdk14中完成交付,所以如果你使用17那就都可以用了,如果你用的11,那对不起不行。可能预览都没得预览。
如果你用的12 13 14 15 16 建议重开,不能选个lts吗。