Java 17 新增的语法特性
说明:本文代码详见《面向实践的Java程序设计教程》教材的代码仓库
JEP与Project Amber
在Java语言的发展过程中,如果需要添加新的语法特性,可以先由发起者以JEP(Java Enhancement Proposal,Java增强提案)文档的形式提出,然后通过初步评估、创建JEP、社区反馈、JEP评审、批准或拒绝、预览等一系列步骤,才可能正式发布。
Open JDK推出的Project Amber项目是用来探索和孵化已经被JEP接受的较小的Java语言特性。本文中所介绍的Java新特性,通常会在后面标上JEP,这是用来说明该语法特性是属于哪个JEP。在搜索引擎中输入JEP编号,即可找到对应的JEP文档。
引言
Java 17于2021年9月14日发布,新增了多个语法特性与类。其中,最值得关注的特性包括:Record、Text Blocks、Pattern Matching for instanceof、Sealed Classes等。这些特性有的是在Java 17发布,有的则是在Java 17之前Java 11之后发布。本文将详细介绍这些新特性,并提供相应的示例代码。
1. Records [JEP 395]
Java 16发布的Record
类型是一种新的数据类型,用于表示不可变的数据,它允许我们创建一个不可变的类型。Record
类型只包含数据字段和访问器方法,不包含可变状态或自定义方法。
通过record
关键字,可以轻松地创建不可变的数据传输对象(DTO)。DTO的英文全称是Data Transfer Object,它是一种用于数据传输的简单POJO(Plain Old Java Object)。Record
类型适用于存储纯粹的值类型数据,如坐标点和只读的日志记录。当你需要一个类型仅用来存储数据的时候,就使用Record
类型。
例子:现在需要定义一个表示点的record类型Point
,该类型包含两个字段x和y,以及相应的访问器方法x()
和y()
。可以使用如下代码:
public class PointRecordMain {
public static void main(String[] args) {
// 定义了record类型,也可以单独定义到一个文件中
record Point(int x, int y) {};
// record类型的使用
Point origin = new Point(0, 0);
Point dest = new Point(0, 0);
System.out.println(origin); // 调用origin自动生成的toString()方法
System.out.println(origin.x()); // 获得x的属性值
System.out.println(origin.y());
System.out.println(origin.equals(dest)); // 比较所有属性的值,返回true
System.out.println(origin == dest); // 返回false,因为比较的是引用地址
}
}
程序输出:
Point[x=0, y=0]
0
0
true
false
代码分析:
Point
虽然没有定义任何的构造方法、getter方法、equals方法等,但是编译器会自动生成这些方法。
toString()
方法:返回包含所有属性的字符串表示形式。
x()、y()
访问器方法:回对应属性的值。
equals
方法:会比较所有属性的值是否相等,类似于使用Objects.equals()方法。这使得record类型实例调用equals方法时,会比较所有属性的值,而不是引用地址。
实际上,使用record关键字定义的类型直接继承自java.lang.Record
类,如果在一个文件中有多个record类型,将会生成对应的多个.class
文件。可以使用javap 对应的字节码文件.class
来查看验证。
record类型可以定义在一个单独的文件中,也可以如例程所示在需要使用的方法中直接定义。record类型还支持定义多个构造方法,以及默认构造方法,详见本书所附代码仓库中的PointRecordMain.java
。
参考资料:
2. switch表达式与yield[JEP 361]
在Java 17中,引入了switch表达式(Switch Expressions)、yield关键字与->
符号。
可以在标签case(label case)中使用yield关键字返回一个值。
示例代码如下所示:
public class SwitchMain {
public static void main(String[] args) {
String day = "Monday";
String r = switch (day) { // switch表达式
case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday" -> {
System.out.println("进入分支1");
yield "工作日"; // 相当于return "工作日"
}
case "Saturday", "Sunday" -> "休息日"; // 无需break与yield,直接返回
default -> throw new IllegalArgumentException("输入错误");
};
System.out.println(day + "是" + r);
}
}
程序输出:
进入分支1
Monday是工作日
在这段代码中,switch表达式返回了一个字符串,并将其赋给变量r。在case分支中,使用yield关键字来返回一个值。这样,switch表达式就可以返回一个值,并且无需使用break来进行中断。如果->
后没有大括号,则表示返回一个表达式。代码case "Saturday", "Sunday" -> "休息日";
中虽然没有使用yield关键字,但是它返回了字符串"休息日"。
注意:switch块中的->
右侧可以是代码块、表达式或者是手动抛出的异常。
3. 用于instanceof的模式匹配[JEP 394]
在Java 16中,就可以使用模式匹配(Pattern Matching)和instanceof关键字来处理类型检查和匹配,可以无需进行显式的类型转换。如下代码所示:
record Person(String name, int age) {
};
public class PatternMatchMain {
public static void main(String[] args) {
Object obj = new Person("李耀", 18);
if (obj instanceof Person p) { // 如为true,则将obj转化为Person类型然后赋给模式变量p
// 无需强制类型转换,直接使用转换后的p
System.out.println(p.age());
} else if (obj instanceof String s) {
// 如果是字符串则调用其toLowerCase()
System.out.println(s.toLowerCase());
}
// instanceof语句后拼接其他条件
if (obj instanceof Person p && p.age() > 16) {
System.out.println("age = " + p.age() + " > " + 16);
}
}
}
程序输出:
18
age = 18 >16
代码obj instanceof Person p
中的Person p
就是类型模式(type pattern
),p是模式变量(pattern variable
)。如果obj instanceof Person
返回true,那么obj就会被转化为Person类型并赋值给变量p。
注意:模式变量只是一个特殊的局部变量,有其作用域,不能在作用域外使用。哪些算是作用域外?无需逐步分析,直接让Java编译器帮我们判定即可。
4. 文本块[JEP 378]
在Java 17中的文本块(Text Blocks)是一种新的字符串字面量,它允许我们在编写多行字符串时更加方便和易于阅读。文本块使用三个反引号(```)来包裹字符串,并且可以在字符串中包含换行符、制表符和其他特殊字符。如下代码所示:
public class TextBlockMain {
public static void main(String[] args) {
String textBlock = """
这是一个"多行字符串"
可以包含制表符\t我前面是制表符
还可以包含换行符\n我前面是换行符
我前面是2个空格。
接下来是一个Person对象的json字符串
{
"name": "John Doe",
"age": 30,
}
""";
System.out.println(textBlock);
}
}
程序输出:
这是一个"多行字符串"
可以包含制表符 我前面是制表符
还可以包含换行符
我前面是换行符
我前面是2个空格。
接下来是一个Person对象的json字符串
{
"name": "John Doe",
"age": 30,
}
文本块常用于编写多行注释和文档字符串,还可用来编写SQL查询语句、JSON字符串等。
在实际编写文本块的时候,可以先在一个文本编辑软件中编写好文本块,然后复制到Java代码中。
更多文本块的用法可参考文章:
5. 密封类[JEP 409]
在Java 17中,密封类(sealed class)允许我们限制一个类的继承范围或一个接口的实现范围,确保它只能被指定的子类继承或指定的类实现。
密封类或密封接口可以有一个或多个允许的子类,这些子类必须是事先指定的,并且必须在同一个模块中声明。比如,某些类你只想让本模块中的其他类继承,而不想让其他模块中的类继承,就可以使用密封类来限制继承范围。
直接继承或实现一个sealed的类型的类只能声明为final, sealed或non-sealed
,参考代码如下:
// 只允许Bird, Aircraft, UFO实现Flyable接口
sealed interface Flyable permits Bird, Aircraft, UFO {
}
/*直接继承或实现一个sealed的类型的类只能声明为final, sealed或non-sealed*/
// 1. 声明为final。其不能有子类。
final class Bird implements Flyable{
}
// 2. 声明为non-sealed。未封闭,其可以有子类
non-sealed class Aircraft implements Flyable{
}
class Helicopter extends Aircraft{
}
// 3. 声明为sealed。封闭,代表其只能拥有permits声明的子类。
sealed class UFO implements Flyable permits CircleUFO, RectUFO{
}
final class CircleUFO extends UFO{
}
final class RectUFO extends UFO{
}
在上述代码中,Flyable接口被声明为sealed,这意味着它只能被Bird, Aircraft, UFO三个类实现。Bird类被声明为final,这意味着它不能有子类。Aircraft类被声明为non-sealed,这意味着它允许有子类。UFO类被声明为sealed,这意味着它只能被CircleUFO, RectUFO两个类实现。
Java 17的其他主要改进
Java 17还有一些其他值得注意的改进,见下文:
稳定版的ZGC垃圾回收器
ZGC垃圾回收器(Z Garbage Collector)是JVM的一种低延迟垃圾回收器,在Java 11作为实验性特性引入。在Java 17中,ZGC成为稳定版,这意味着它将包含在JDK中,并且可以用于生产环境。ZGC作为JVM可以达到亚毫秒级停顿,不过需要手动开启。
主流Java开发框架支持
Spring Framework 6
和 Spring Boot 3
框架需要基于Java 17。
更清晰的NullPointerException异常信息提示
Java 17中对于NullPointerException异常信息提示进行了改进,以前的提示只能定位到行,Java 17中则能定位到具体某个变量。
以如下代码进行说明:
public class NullPointerExceptionMessageDemo {
public static void main(String[] args) {
String str = null;
System.out.println(str.length());
}
}
运行后,会得到如下异常信息提示:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "str" is null
可以看到,异常信息提示中的because "str" is null
已经明确指出str是null。
Stream接口新增toList()方法
在Java 17中Stream接口正式增加了toList()
方法,可以方便地将Stream转换为List,而无需使用Collectors.toList()
方法。
如下代码所示:
import java.util.List;
// import java.util.stream.Collectors;
import java.util.stream.Stream;
public class StreamToListDemo {
public static void main(String[] args) {
Stream<Integer> scoreStream = Stream.of(98, 97, 90);
List<Integer> scoreList = scoreStream.toList();
// Java 11中需要使用Collectors收集器
// List<Integer> list = scoreStream.collect(Collectors.toList());
for (Integer e : scoreList) {
System.out.print(e+" ");
}
}
}