本文是《Java语言新特性漫谈》系列文章中的一篇,该系列文章主要探讨各 Java 版本的语言特性方面的增强更新。
Switch表达式
Switch 表达式的相关改进特性是从 Java 12 开始引入的预览特性,经过 Java 13 的一点微调,Java 14 终于成为正式版本。
主要的改进包括:
- 新的
case
分支语法:case ... -> labels
(该写法不存在“条件贯穿”); switch
既可以作为语句,也可以作为表达式。作为表达式时,使用yield
返回表达式的值。
可以分别参考之前的两篇介绍《Java语言新特性漫谈:Java 12之switch》以及《Java语言新特性漫谈:Java 13篇》
文本域(预览特性第2版)
Java 14 在 Java 13 文本域的预览特性基础上,加入了更多的转义字符支持:
\
:续行符。提供源代码级别的换行。\s
:提供保留行尾空格的能力。
文本块中的换行会导致在代表的文本中插入换行,那么,如果文本行超长,仅仅想在源代码级别换行该怎么办呢?答案是使用“续行符”。
在行末插入“续行符”,以 \
反斜杠表示。如:
1 | String singleLine = """ |
大多数语言的续行符都是反斜杠。
另一方面,文本域在自动格式化的时,会移除行尾的空格。
如果不想结尾空白字符被移除,可以 \s
或 \040
结尾。
注意,不能使用
\u0020
,因为 Unicode 转义解析发生在词法分析之前。
如果不想结尾空白字符被意外移除,-Xlint:text-blocks
同样可做该项检查,移除发生时会警告trailing white space will be removed
。
instanceof
操作符的模式匹配(预览特性)
这个特性名称听起来很高大上,其实涉及的只是 instanceof
操作符的很小一个改进。
语法
通常,instanceof
操作符用以判断一个变量是否是某种类型,如果是,就转型为该类型进行处理——这就是大家熟悉的“instanceof-and-cast 习惯用法”。比如下面这段计算矩形周长的代码:
1 | if (shape instanceof Rectangle) { |
很容易发现,第 2 行那条转型语句通常就是那样固定,写了还可能写错,那有什么必要写呢?所以,那就简化掉吧。现在可以这样写了:
1 | if (shape instanceof Rectangle s) { |
可以看到,现在我们在使用 instanceof
进行类型测试的同时,就声明了一个变量——如果类型测试的结果为 true
,就会将被测对象转型后赋值给新声明的变量——这样就可以直接使用,而不再需要手动的类型转换了。
用法就介绍完了!是不是很简单!
变量作用域
一个值得讨论的问题是:新声明的变量作用域有多大?简单来说就两点:
- 在
instanceof
语句所在的代码块内; - 在类型测试结果为
true
的条件下能执行到的代码中。
一个容易忽略的场景是在短路逻辑中,比如下面这代码是合法的:
1 | if (shape instanceof Rectangle s && s.length() > 5) { |
因此,只有当 shape instanceof Rectangle s
为 true
时,才会执行 s.length() > 5
。
反过来,如果换成“或”就不合法了,比如:
1 | if (shape instanceof Rectangle s || s.length() > 0) { // error |
此时,即使类型测试结果为 false
,也是会执行后面的代码的。
模式匹配为何?
该特性叫“模式匹配(pattern match)”,很学术的一个概念,那到底什么是模式匹配呢?
模式匹配用于测试一个对象是否有某种特定结构,如有则从中提取数据。
这不正是 instanceof
操作符的作用么!
模式(pattern),是一个谓词和一系列绑定变量的组合。其中,谓词可以应用于一个目标。而只要谓词匹配,就可以从目标中提取绑定变量。
谓词(predicate),是一个仅带一个参数的布尔函数。
在本特性中,谓词就是instanceof
测试变量类型,目标即是被测变量,绑定变量显然就是新声明的变量。只要测试结果为true
,被测变量就会被转型赋值给新声明的变量。
记录(Record,预览特性)
终于,Java 又新增了一种新的类型声明——记录(Record)。跟 enum
一样,record
也是一种受限的类。
通常,它用作数据载体。
与其说 record
是一种新类型,不如说它是一种类模板,使用者仅需提供少量与数据相关的信息,编译器就会根据这些信息自动生成相关方法。因此,其语法很简洁,看起来是这样子的:
1 | record Rectangle(float length, float width) { } |
注意,紧随类名后,就罗列了所有的数据字段。
看起来很像声明了一个方法,但实际上不是。
从上述 Rectangle
类声明中,编译器会自动生成些什么呢?
private final
修饰的数据字段(这里有两个字段length
和width
)- 与数据字段同名且返回类型一致的
getter
访问器(也是两个) - 从数据字段推断出的公共构造器(显然包含两个参数)
- 实现
equals()
和hashCode()
方法,判等逻辑是所有数据字段类型及值均相等 - 实现
toString()
方法,包含类名及所有数据字段的名称及对应的值
关于访问器注意两点:
- 没有
setter
访问器,因此可以认为记录是不可变的。但是,“不可变”不是绝对的,如果数据字段是某些引用类型,比如集合,其中的内容是可以改变的。 getter
访问器允许自定义。
另外,构造器是可以重载的,只是重载的构造器第一行必需要调用另一个构造器。比如:
1 | record Rectangle(float length, float width) { |
Compact构造器
如果没有编写构造器,构造器是由编译器推断生成的。但并不是说不可以自定义构造器,只是在构造器中不能再给数据字段赋值,只能做诸如数据字段的合法性校验等操作。
或许是因为能力有限,才称为“Compact 构造器”吧!
尝试在自定义的构造器中给数据字段赋值,会得到一个“无法为最终变量xxx分配值”的错误。
构造器声明语法如下:
1 | record Rectangle(float length, float width) { |
呃……类声明语法像方法,构造器语法像类声明,也是够了!
限制
一开始就说明了,记录是一种受限的类。那到底有什么限制呢?
- 记录不能派生任何类,反过来,任何类也不能继承记录,即它是
final
的 - 记录不能是
abstract
的,是隐式final
的 - 记录不能声明实例变量(其实例变量均只能是数据字段)以及实例初始化器
- 记录的数据字段也都是隐式
final
的
1 | record Rectangle(double length, double width) { |
记录类的超类是
java.lang.Record
。
除去这些限制,记录与普通类很相似,它可以:
- 内部可声明嵌套类和接口(包括嵌套记录类),不过它们是隐式
static
的 - 可以创建泛型记录
- 可以实现接口
- 使用
new
关键字创建实例 - 内部可声明:静态方法、静态变量、静态初始化器、构造器、实例方法以及嵌套类型
- 可以注解记录和它的各个数据字段
1 | // 泛型记录 |
注意,注解是同时应用于:规范构造器参数、记录类组件、私有字段和访问器方法等,除非声明中有指定注解应用的目标(
@Target
)。
API更新
如果不查看一个类的声明,单从名称上并不能看出它是否是记录。因此,必然需要一种方式来识别。
Class
为此新增了两个方法,boolean isRecord()
用以判断一个类是否是记录,如果是记录,RecordComponent[] getRecordComponents()
会返回其数据字段信息,否则返回 null
。