Java 12中新的switch表达式
JDK 12已经于19年3月发布了。这是Java 9发布时宣称采用6个月作为一个发布周期以来的第三个版本。这次又给我们带来了什么新东西?本文主要想介绍下预览模式下的一个新的语言特性:switch表达式。下篇文章我们会讲下JDK的变化,包括G1及Shenandoah垃圾回收器。
switch表达式
对那些喜欢新的语言特性的人来说,Java 12带来的加强版的switch表达式一定能让你们满意。不过这个特性当前只在预览模式中可用。也就是说如果你和往常一样直接运行Java编译器,新的switch表达式语法是不支持的。你需要在编译代码的时候加上--enable-preview和--release 12才能启用这一特性。要想编译本文中的代码片段,需要确认你已经安装好了JDK12,并且执行了如下的命令:
javac --enable-preview --release 12 Example.java
要运行生成后的class文件的话,你还得将 --enable-preview这个标记传入给Java启动器:
java --enable-preview Example
在了解新特性之前,我们先来看下什么是预览模式。根据官方文档,“语言或虚拟机的预览特性指的是Java SE平台上完整阐明、完全实现的临时特性。它是JDK发布版本中的可用特性,旨在获取开发人员实际使用中的反馈信息;这可能使得它在未来的Java SE版本中成为正式的特性。也就是说,它是可以被优化或者移除的。”[重点补充——Ed.]
那我们现在使用的switch表达式有什么问题吗?我们将会谈到的有四点提升:穿透执行(fall-through)问题,组合形式,穷举性(exhaustiveness),以及表达式形式。
穿透执行(fall-through)问题
我们先来看一个fall-through的例子。在Java里,经常会这样写switch语句:
switch(event) {
case PLAY:
//do something
break;
case STOP:
//do something
break;
default:
//do something
break; }
注意每个case条件都由代码块中的一个专门的break语句来处理。break语句会确保switch语句中的下一个代码块不会被执行到。那如果你漏掉了break语句会发生什么?代码还能编译过吗?当然可以。做个小测试,来猜猜看下面代码会输出什么结果:
var event = Event.PLAY;
switch (event) {
case PLAY:
System.out.println("PLAY event!");
case STOP:
System.out.println("STOP event");
default:
System.out.println("Unknown event");
}
这段代码会打印出:
PLAY event!
STOP event
Unknown event
switch的这个行为就叫穿透执行。正如Oracle的Java SE文档中所说的,“匹配到的case标签后的所有语句都会顺序执行,后续的case标签都会被忽略掉,除非碰到break语句。”
如果你忘记了break语句,穿透执行行为会导致难以发现的bug。因此,程序的执行结果可能是不正确的。事实上,如果你编译的时候带上了-Xint:fallthrough标记,Java编译器是会提醒你存在可疑的fall-through问题的。像Error Prone这样的代码检查器也能识别出这类问题。
JDK增强提案(JDK Enhancement Proposal,JEP)325中也提到了这个问题,这也是要增强switch语句的一个原因:“Java switch语句目前的设计和C、C++这样的语言走的很近,并默认支持fall-through语义。而这种传统的控制流在编写底层代码时通常是非常有用的(比如二进制编码的解析器),但当它用于更高级的场景中时,它这容易出错的问题远比能带来的灵活性要多得多。”
现在在Java 12中(要启用with --enable-preview选项),终于有不存在fall-through的新的switch语法了,这样也可以减少出bug的可能性。下面展示了如何使用新的switch语法来优化原先的代码:
switch (event) {
case PLAY -> System.out.println("PLAY event!");
case STOP -> System.out.println("STOP event");
default -> System.out.println("Unknown event");
};
新的switch形式使用了Java 8中引入的lambda风格的语法,用箭头来连接标签和返回值的代码段。注意到这并不是真的lambda表达式;只是语法上很像。你可以使用单行的表达式也可以使用大括号包起来的代码块,就和在lambda表达式里使用一样。下面的例子展示了同时使用了单行表达式和大括号块的混合语法。
switch (event) {
case PLAY -> {
System.out.println("PLAY event!");
counter++; }
case STOP -> System.out.println("STOP event");
default -> System.out.println("Unknown event");
};
复合case
下一个就是如何处理多case标签了。在Java 12之前,你只能一次处理一个case标签。比如说在下面的代码中,尽管STOP和PAUSE的逻辑是一样的,你还是需要使用两个单独的case语句,除非你使用了fall-through特性:
switch (event) {
case PLAY:
System.out.println("User has triggered the play button");
break;
case STOP:
System.out.println("User needs to relax");
break;
case PAUSE:
System.out.println("User needs to relax");
break;
}
常见的做法是使用switch的fall-through语义来简化代码:
switch (event) {
case PLAY:
System.out.println("User has triggered the play button");
break;
case STOP:
case PAUSE:
System.out.println("User needs to relax");
break;
}
然而,正如前面所说的,这种风格容易导致bug,因为分不清楚到底是缺少了break语句,还是故意这样写的。如果有办法指定说对STOP和PAUSE条件来说处理逻辑是一样的,这样就清晰多了。这正是Java 12现在支持的。使用箭头形式的语法,你可以指定多个case标签。前述代码可以优化成这样:
switch (event) {
case PLAY ->
System.out.println("User has triggered the play button");
case STOP, PAUSE ->
System.out.println("User needs to relax");
};
在这段代码里,只是简单地把标签连续罗列出来就好了。现在这段代码更简洁也更清晰了。
穷举性
另一个好处是新的switch语句的穷举性。也就是说当你在枚举上使用switch语句时,编译器会检查是否每个枚举值都有一个匹配的switch标签。
比如说,如果在这么一个枚举类型:
public enum Event {
PLAY, PAUSE, STOP
}
然后你写了个switch语句,覆盖了其中一些值,比如这样:
switch (event) {
case PLAY -> System.out.println("User has triggered the play button");
case STOP -> System.out.println("User needs to relax");
}; // 编译错误
在Java 12中,编译器会生成这样的报错:
error: the switch expression does not cover all possible input values.
这个报错就很有用了,可以提示你缺少了一个default子句,或者忘了处理所有可能的值。
表达式形式
表达式的形式也是相对于老的switch语句的一个改进:为了理解什么是“表达式形式”,我们有必要先回顾下语句(statement)和表达式(expression)的区别。
语句指的是“动作”。而表达式,是产生值的一个“请求”。表达式是基础,也易于理解,这使得代码也更容易理解,可维护性更强。
在Java中,可以很清晰地看到if语句和三元运算符的区别,后者是一个表达式。下面的代码凸显了它们的不同之处。
String message = "";
if (condition) {
message = "Hello!";
}
else {
message = "Welcome!";
}
这段代码可以重写成下面的表达式:
String message = condition ? "Hello!" : "Welcome!";
在Java 12之前,switch只是一个语句。而现在,你可以使用switch表达式。举个例子,先看下这段处理不同的用户事件的代码:
String message = "No event to log";
switch (event) {
case PLAY:
message = "User has triggered the play button";
break;
case STOP:
message = "User needs a break";
break;
}
这段代码可以重写成更简洁的表达式的形式,这样能更好地表达出代码本身的意图:
var log = switch (event) {
case PLAY -> "User has triggered the play button";
case STOP -> "User needs a break";
default -> "No event to log";
};
注意看下现在switch是如何返回值的——它已经是一个表达式了。这种switch的表达式形还能让你可以在case块中使用return语句。不过,如果switch里的代码过于复杂难以理解了,我们更推荐将复杂的代码逻辑抽到一个私有的良好命名的工具方法里。你只需使用表达式风格的语法,简单调用下这个方法就好了。
走进模式匹配
你可能会想知道为什么需要一种新的语言特性。从Java 8开始,Java的发展很明显受到了函数式编程的影响:
- Java 8引入了lambda表达式及流(stream);
- Java 9带来了支持响应式流的Flow API;
- Java 10新增了局部变量类型推导。
这些想法早就存在于Scala和Haskell这类的函数式编程语言中了。那Java这里有什么新的想法?这就是(结构化)模式匹配,不要和正则表达式混淆了。
这里所说的模式匹配是指你用来测试一个对象是否满足某个特定的结构,然后再决定将这个结构中的特定部分提取出来做后续处理。在Java里这通常是通过将instanceof检查和case表达式组合的方式来完成。一个典型的使用场景就是当你要为某个特定领域的语言编写解析器的时候。比如说,下面的代码会检查一个对象是否是某个特定的类型,是的话再进行类型转换,然后便可以从中提取信息。
if(o instanceof BinOp) {
var binop = (BinOp) o;
// use specific methods of the BinOp class
}
else if (o instanceof Number) {
var number = (Number) o;
// use specific methods of the Number class
}
Java还没有一种语言特性能支持完整的模式匹配,不过目前它作为一个潜在的候选项正在讨论之中,正如JEP 305中所描述的。
总结
Java 12没有带来任何易于使用的新的语言特性。不过,在预览版本的语言特性中,它引入了switch表达式。switch表达式是一个有用的补充,它能让你编写出更简洁和更不易出错的代码。特别是它提供的这四项改进:fall-through语义,组合形式,穷举性,以及表达式形式。最后,也许会更让人激动的是,switch支持的语法正变得越来越丰富。
目前在Java 12中,switch语句只能支持enum, String, byte, short, char, int以及它们的包装类。不过在未来,也许能支持更复杂的形式,对任意“switch支持”的类型也可能会提供结构化的模式匹配。