Java 12将在两个月后(2019/3/19)发布,现已进入RDP1阶段,确定加入8个JEP。其中对Java语法的改进是JEP 325: switch表达式。于是我迫不及待,提前感受一下更先进的语言特性。
因为12没有正式发布,本文使用自己编译的OpenJDK。嫌麻烦的话,也可以直接使用官方的ea版本。JEP325是预览(preview)特性,编译运行时需要添加--enable-preview
参数。
顾名思义,这个feature是对switch
动手脚的。包括两个方面。
1. 简化fall-through规则
下面这样的switch
代码我们写过几万遍了
switch (today) { case SATURDAY: case SUNDAY: System.out.println("I'm happy!"); break; case MONDAY: case TUESDAY: case WEDNESDAY: case THURSDAY: case FRIDAY: System.out.println("I'm sad..."); break; default: System.out.println("I'm confused."); }
这段代码存在的问题是:
1. 内容不符合爱岗敬业的核心价值观(敲黑板!重要!!)
2. 多个条件对应相同代码时(比如MONDAY到FRIDAY),要重复写多个case
,冗余且丑陋
3. 每一段代码后面都要有break
,一旦忘记就会有编译器检测不到的逻辑错误
4. 变量作用域混乱
第四个问题可能长被忽略。case
或者default
后面是一连串的语句,而不是代码块(注意,它是没有大括号的)。这种情况下定义的局部变量,其作用域不是case
后的部分,而是整个switch
结构。因此,下面的代码无法通过编译。
switch (today) { case MODAY: int x = 1; break; default: int x = 0; //Variable x is already defined in the scope }
编译器看到的是在一个作用域中存在两个x
,非常违背人类的直觉。
上面的四个问题,除了1,剩下的万恶之源就是fall-through规则。即switch
结构在找到第一个匹配的case
条件后,会顺序执行后面所有case
对应的代码,无论是否判断为真。这是40多年前C语言创造后来Java原样照抄的经典语法,但在今天看起来就显得很呆萌了,新的语言也几乎都放弃了fall-through。
好在,尽管后知后觉,从12开始Java开发者也可以选择更简洁清晰的语法了。就像这样
switch (today) { case SUNDAY, SATURDAY -> System.out.println("I'm happy!"); case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> System.out.println("I'm happy, too!!"); default -> System.out.println("I'm confused."); }
很容易看出语法的变化,这些变化也解决了上面的四个问题。归纳一下:
![](/qrcode.jpg)
1. 程序内容积极向上,体现了新时代的奋斗精神(敲黑板!重要!!)
2. 对应相同动作的多个case
合并为一行,代码更简洁
3. 条件和动作之间用->
连接,这时fall-through规则失效。匹配到的分支代码执行完后直接跳出,不会继续执行下面的case
对应的代码。也就是不需要再为每一个分支写break
了。程序更简洁清晰,也更符合人类的直觉。
需要注意,为了保持向后兼容性,case
条件后依然可以使用:
,这时fall-through还是有效的,即不能省略原有的break
。而一个switch
结构里不能混用->
和:
,否则会有编译错误。
4. 每一个->
后面只允许接一个表达式、一个代码块、或者一个throw
语句。这样在代码块中定义的局部变量,其作用域就限制在代码块中,而不是蔓延到整个switch
结构。逻辑更加清楚了。
2. switch作为表达式(expression)
switch
结构一直是一个statement,而从Java 12开始,它也可以用作expression。从学院派的定义理解statement和expression的区别叫人头疼,如果说人话的话,就是switch
可以有返回值了。
作为statement的switch
没有返回值,所以我们不能写出这样的代码
x = switch (y) { ... }
如果需要根据不同的条件给某个变量赋值,我们以前只能这样做
String word = "";
switch (num) { case 1: word = "One"; break; case 2: word = "Two"; break; default: String result = String.format("Other (%d)", num); word = result; }
让人难受的地方有两个。
1. 重复多次地写赋值语句,繁琐且易错。
2. 这段程序的终极目标是为word变量
赋值,而赋值前必须在其他的地方初始化word
,淡化了二者的逻辑关系,代码也显得琐碎。
从12开始我们可以这样改造代码
String word = switch (num) { case 1 -> "One"; case 2 -> "Two"; default -> { String result = String.format("Other (%d)", num); break result; } };
可见,switch
成了一个表达式(expression),它有自己的返回值。每一个分支只需要决定具体的返回值是什么,不需要考虑如何使用这个值。而全程只需要一次赋值操作。代码整体变得更简洁、紧凑、清晰。
而返回值又有两种写法。还记得吗,上一节提到过,->
后只能接三样东西:表达式、代码块、throw语句。throw
的情况没有返回值,先不管它。另外两种情况:
1. 如果分支只有一个表达式,那么表达式本身就是switch
的值,比如上面例子里的"One"
和"Two"
;
2. 如果分支是一个代码块,比如例子中的default
,可以看到Java 12改造了break
关键字,可以通过break result
的形式返回值。switch
并没有抛弃break
,而是赋予它更重要的职能。
作为expression的switch
也可以使用:
,在这种情况下,各个分支必须用break
关键字返回值。像这样
String word = switch (num) { case 1 : break "One"; case 2 : break "Two"; default : { String result = String.format("Other (%d)", num); break result; } };
上面例子中,case 1
和case 2
中的break
不能省略,否则会有编译错误。
很显然,当switch
用作expression时,每一个分支都必须有返回值(或者有throw
异常)。我们不能写下面这样的代码
String word = switch (num) { case 1 -> "One"; case 2 -> "Two"; default -> { System.out.println("莫挨老子"); //错误: switch rule completes without providing a value } };
编译器不知道当num=3的时候应该返回什么,于是它愤怒地抛出了一个错误。
最后要强调,switch
在不返回值的时候,还是一个statement。而作为expression并且在一句代码的结尾处时,不要忘了后面的分号!(亲自踩坑,友情提醒)
To be continue...
可能你会觉得这些改进还是小修小改,不值得过分激动。但是,JEP 325是JEP 305: Pattern Matching的依赖。虽然没有最终确定,但或许Pattern Matching会在不久后的几个版本正式引入,到时又将是语言层面的大革命。后续的几个版本还是值得期待的。