前言
2020年3月17日,Java 14 发布。通过这篇文章我们来看下它新增的一些特性,便于我们更好的了解及掌握。
正文
PS: 预览特性表示改特性被放到该版本后,可以正常使用,但在以后版本可能会有改动/删除(也可能不再改动/删除)。
JEP 305 – instanceof 匹配模式(预览特性)
instanceof操作符的改进
我们以一个例子来看下instanceof
操作符的改进。
- 旧版本实现
如果应用程序需要处理某种类型的类,但我们有父类类型的引用,那么我们需要检查该实例的类型并进行适当的类型转换。
例如,Customer
的类型可以是BusinessCustomer
或PersonalCustomer
。根据客户实例的类型,我们可以根据上下文获取信息。
1 | Customer customer = new PersonalCustomer(); //通过某些方法拿到数据,略 |
- 新版本实现
现在,通过与instanceof
进行模式匹配,我们可以用以下方式编写类似的代码。在这里,我们可以减少类型转换的样板代码。
1 | //新版本实现 |
相关细节
- 类型测试模式
类型测试模式包含以下内容:
- 可以应用于目标的谓词
- 只有在谓词成功应用于目标时才从目标提取的一组绑定变量
类型测试模式由指定类型的谓词和单个绑定变量组成。
在下面的代码中,短语字符串s
是类型测试模式:
1 | if (obj instanceof String s) { |
- 它做了什么
如果obj
是String
的实例,则instanceof
操作符将目标obj
“匹配”到类型测试模式,然后将其转换为String
并分配给绑定变量s
。
需要注意的是,只有当obj
不为空时,模式才会匹配,s
才会被赋值。
- 复杂表达式用法
当if
语句变得更加复杂时,绑定变量的作用域也相应增大。
例如,当我们添加&&
运算符和另一条语句时,只有instanceof
成功并分配给pCustomer
时,添加的语句才会被计算。另外,true
块中的pCustomer
引用了所包含类中的一个字段。
1 | //可以执行 |
与上面的情况相反,当我们添加||
操作符和另一条语句时,绑定变量pCustomer
不在||
操作符右手边的作用域内,也不在true
块的作用域内。在这些点上,pCustomer
引用了封闭类中的一个字段。
1 | //编译错误 :: The pattern variable pCustomer is not in scope in this location |
JEP 368 – 文本块(二次预览特性)
简介
在Java中,文本块是一个多行字符串字面量。这意味着我们不需要陷入显式的行结束符、字符串连接和分隔符的混乱就可以编写普通字符串文本。
Java 文本块在 Java 13 (JEP 355) 和 Java 14 (JEP 368) 中作为预览特性可用。它计划成为Java 15 (JEP 378) 中的一个标准特性。
要启用这个预览特性,我们必须使用-enable-preview
和-source 14
标志。
文本块语法
- 文本块由多行文本组成,并使用三个双引号字符(“ “ “)作为开始和结束分隔符。
- 开始的三个双引号字符后面总是跟一个行结束符。
- 我们不能将分隔符和文本块放在一行上。开始的分隔符必须在它自己的行上。内容只能从下一行开始。
- 如果文本内容包含单引号或双引号,则不需要对它们进行转义。
1 | String dbSchema = """ |
看起来十分简单,我们再深入了解一下。
与String相似之处
- 从文本块生成的实例的类型是
java.lang.String
。具有与传统双引号字符串相同特征的字符串。这包括对象表示和在字符串池中的表现。 - 我们可以使用文本块作为
String
类型的方法参数传递。 - 文本块可以在任何可以使用字符串文字的地方使用。例如,我们可以将它用于字符串连接。
1 | String string = "Hello"; |
输出:
1 | Hello |
缩进
- 偶然缩进和基本缩进
文本块保留其内容的缩进。为了执行此操作,JEP将空格分为偶然缩进和基本缩进。
让我们来看下第一个例子:
1 | String dbSchema = """ |
输出:
1 | |CREATE TABLE 'TEST'.'EMPLOYEE' |
这里,我们有两种类型的缩进:
第一个缩进是从行开始到所有行中的单词“CREATE”为止。这可以根据各种因素增加或减少,比如格式化插件或开发人员的选择。这是偶然的缩进。
第二次缩进是从字符’(‘到’ID’。大部分空间是4到8个空格。这样做是为了保持文本块的缩进意图。这被称为基本缩进。
Java文本块删除所有偶然的缩进,只保留基本的缩进。
- 添加自定义的缩进
让我们想象一下,在上面的例子中,我们想要给所有行的左边两个制表符缩进。要做到这一点,我们可以将关闭的三重引号精确地向左移动两个制表符。放置的位置与应起压痕的位置完全相同。
1 | String dbSchema = """ |
输出:
1 | | CREATE TABLE 'TEST'.'EMPLOYEE' |
另外,请注意文本块中每行末尾的空格也会被Java编译器除去。
- TAB的处理
对于编译器来说,了解制表符在不同编辑器中是如何显示的并不困难。
编译器将单个空格字符视为单个制表符,即使制表符可能会产生相当于8个空格的空白。
行终止符
不同的平台有不同的行结束符。Java不进行平台检测,并将文本块中的所有行终止符规范化为\n
。
如果需要平台行终止符,则可以使用String::replaceAll("\n",System.lineSeparator())
。
1 | String string = "Hello"; |
新转义符
- 避开换行符
很多时候,我们只希望将内容写入程序中的多行,但它们实际上是单个字符串内容。在这种情况下,我们可以使用行结束符转义字符,即单反斜杠’'。它禁止包含隐式换行字符。
1 | String dbSchema = """ |
输出结果:
1 | |CREATE TABLE 'TEST'.'EMPLOYEE'('ID' INT NOT NULL DEFAULT 0 ,'FIRST_NAME' VARCHAR(100) NOT NULL , |
- 右侧空格补充
如果出于某种原因不想去掉缩进,可以使用’\s’ (ASCII字符32,空格)转义序列。在任何一行的末尾使用它可以保证一行在遇到’\s’之前都有空格字符。
1 | String dbSchema = """ |
输出结果:
注:为便于理解,我们用’.’替换了所有空格。
1 | CREATE.TABLE.'TEST'.'EMPLOYEE'........... |
使用场景
- 只有在提高代码的清晰度时才使用文本块,特别是对于多行字符串。
- 如果字符串符合使用条件,请始终使用字符串。它们在应用程序性能方面更好。
- 为了保持所需的缩进,始终使用相对于内容的最后一行的三引号结束位置。
- 避免在复杂表达式(如lambda表达式或流操作)中出现内联文本块,以保持可读性。可考虑重构为局部变量或静态
final
字段。 - 文本块的缩进只使用空格或制表符。混合使用会导致文本对齐出现问题。
JEP 358 - 空指针问题定位
Java 14 通过精确地描述哪个变量为null
,提高了由JVM生成的NullPointerException
的可用性。
首先,我们需要传递-XX:+ShowCodeDetailsInExceptionMessages
JVM参数,以便在运行应用程序时启用该特性。
1 | public class HelpfulNullPointerException |
输出错误日志:
1 | Exception in thread "main" java.lang.NullPointerException: |
可以看到现在日志清晰的告诉我们哪个方法哪个变量为null
而引发的异常。
没有此特性之前的输出日志:
1 | Exception in thread "main" java.lang.NullPointerException |
注意点:
- 只有由JVM直接创建和抛出的NPEs才会包含
null
的细节消息(当我们在程序中创建异常时,通常会在构造函数中传递这些消息)。运行在JVM上的程序显式地创建和(或)抛出的NPEs不受字节码分析的影响。 - 请注意,由于一些原因,可能在所有情况下都不需要
null
细节消息。例如,它会影响性能,因为算法会给堆栈跟踪的生成增加一些开销。 - 此外,它还增加了安全风险,因为
null
细节消息提供了对源代码的洞察,否则就不容易获得这些信息。
JEP 359 - record(预览特性)
Java 中的record
类型。它是在 Java 14 中作为预览特性引入的,用于修饰普通的不可变数据类,实现类和应用程序之间的数据传输。
record 类型
与enum
一样,record
也是 Java 中的一种特殊类类型。它的目的是用于创建类仅作为纯数据载体的地方。
class
和record
之间的重要区别在于,record
旨在消除设置和从实例获取数据所需的所有样板代码。record
将这个责任转移给Java编译器,Java编译器帮我们生成构造函数、字段getter
、hashCode()
和equals()
以及toString()
等方法。
我们可以在record
定义中覆盖上面提供的任何默认方法来实现自定义行为。
- 语法
使用关键字record
在Java中创建这样的record
类。就像我们在构造函数中所做的一样,我们需要在record
中设置属性和它们的类型。
在给定的示例中,EmployeeRecord
用于保存员工信息,如下。
1 | package com.howtodoinjava.core.basic; |
- 创建和使用
要创建一个record
,需要调用它的构造函数并将所有字段信息传递进去。然后,我们可以使用JVM生成的getter
方法获取记录信息,并调用任何生成的方法。
1 | package com.howtodoinjava.core.basic; |
输出结果:
1 | 1 |
- 底层原理
当我们创建EmployeeRecord
记录时,编译器创建字节代码并在生成的类文件中包括以下内容:
- 一个包含所有字段的构造函数。
toString()
方法的作用是:打印record
中所有字段的状态/值。equals()
和hashCode()
方法使用基于 动态反射(invokedynamic)的机制。getter
方法的名字类似于字段名,例如id()
,firstName()
,lastName()
,email()
和age()
。- 这个类继承自
java.lang.Record
,它是所有record
的基类。这意味着record
不能继承其他类。 - 这个类被标记为
final
类型,这意味着我们不能创建它的子类。 - 它没有任何
setter
方法,这意味着record
实例被设计为不可变的。
如果我们在生成的类文件上运行javap
工具,我们将看到类文件的相关内容。
1 | public final class com.howtodoinjava.core.basic.EmployeeRecord |
使用 record 的场景
- 在建模诸如领域模型类(可能通过ORM持久化)或数据传输对象(DTO)之类的东西时,
record
是理想的候选对象。 - 这些
record
在临时存储数据时很有用。例如,可以在JSON反序列化期间。通常在反序列化期间,我们不期望程序改变从JSON读取的数据。我们只是读取数据并将其传递给数据处理器或验证器。 - 另外,
record
不能替换可变Java bean,因为record
在设计上是不可变的。 - 当一个类打算保存数据一段时间并且希望避免编写大量样板代码时,请使用
record
。 - 我们可以在各种其他情况下使用
record
,例如保存方法、流连接、复合键的多个返回值,以及在数据结构(如树节点)中使用记录。
原理深度分析
- 动态反射(invokedynamic)
如果我们看Java编译器生成的字节码来检查toString()
(以及equals()
和hashCode()
)的方法实现,那么它们是使用基于invokedynamic
的机制实现的。
invokedynamic
是一个字节码指令,它通过动态方法调用来实现动态语言(针对JVM)的相关功能。
- 无法被继承实现子类化
尽管所有record
都继承了java.lang.Record
类,我们仍然不能显式创建java.lang.Record
的子类,编译器不会通过。
1 | final class Data extends Record { |
这意味着获得record
的唯一方法是显式地声明一个record
,并让javac
创建类文件。
- 使用注解
我们可以向记录的组件添加适用于它们的注释。例如,我们可以对id
字段应用@Transient
注解。
1 | public record EmployeeRecord( |
- 序列化
record
的Java序列化与常规类的序列化不同。record
对象的序列化形式是从该对象的最终实例字段派生的值序列。record
对象的流格式与流中的普通对象的流格式相同。
在反序列化中,如果指定流类描述符的本地类等价于一个record
类,则首先读取并重新构建流字段,以作为record
的组件值;其次,通过以组件值作为参数(或者如果流中缺少组件值,则为组件类型的默认值)调用record
的规范构造函数来创建record
对象。
除非是显式声明,否则record
类的serialVersionUID
为0L
。对于record
,也无需匹配serialVersionUID
值。
无法自定义用于序列化record
对象的过程;在序列化和反序列化期间,record
类定义的任何特定于类的writeObject
、readObject
、readObjectNoData
、readResolve
、writeExternal
和readExternal
方法都会被忽略。但是,writeReplace
方法可用于返回要序列化的替代对象。
在执行任何序列化或反序列化之前,我们必须确保record
必须是可序列化或可外部化的。
1 | import java.io.Serializable; |
输出结果:
1 | EmployeeRecord[id=1, firstName=Lokesh, lastName=Gupta, |
- 其他字段和方法
可以添加新的字段和方法,但不建议添加。
添加到record
的新字段(未添加到组件列表中)必须是静态的。也可以添加一个方法来访问记录字段的内部状态。
添加的字段和方法不会在编译器隐式生成的字节代码中使用,因此它们不是equals()
、hashCode()
或toString()
等任何方法实现的一部分。我们必须根据需要显式地使用它们。
1 | public record EmployeeRecord( |
- Compact Constructor
我们可以在Compact Constructor中添加用于数据验证的构造函数特定代码。它有助于构建在给定业务上下文中有效的记录。
Compact Constructor不会导致编译器生成单独的构造函数。相反,在Compact Constructor中指定的代码将作为额外代码出现在规范构造函数的开始处。
我们不需要指定构造函数参数给字段的赋值,就像在规范构造函数中通常发生的那样。
1 | public record EmployeeRecord( |
API变化
- Class class
Class
类有两个方法—isRecord()
和getRecordComponents()
。getRecordComponents()
方法返回一个RecordComponent
对象数组。
RecordComponent
是java.lang.reflect
包中的一个新类。它包含十一个方法,用于检索注释和泛型类型的详细信息。
- ElementType 枚举
ElementType
为 record
新增了一个常量,RECORD_COMPONENT
。
- javax.lang.model.element
ElementKind
枚举为record
新增了三个新的常量和instanceof
特性的模式匹配,即BINDING_VARIABLE
、RECORD
和RECORD_COMPONENT
。
结语
Java record
是一个非常有用的特性,对Java类型系统是一个很好的补充,有助于几乎完全地减少为简单数据载体类编写的样板代码。
但是我们使用时应当注意,不要试图自定义它的行为,最好使用默认的构造。
JEP 361 - switch 表达式(标准)
Java 14 中switch
语句允许程序在运行时根据给定表达式的值有多个可能的执行路径。
求值表达式称为选择器表达式,它的类型必须是char
、byte
、short
、int
、Character
、Byte
、Short
、Integer
、String
或enum
。
在Java 14中,使用
switch
表达式,整个switch
块“获得一个值”,然后可以在同一语句中将该值赋给一个变量。
- 例子
- 在Java 14中,它是一个标准特性。在Java 13和Java 12中,它作为预览特性。
- 它支持多个
case
标签,并使用yield
来代替旧的return
关键字返回值。 - 它还支持通过标签规则返回值(类似于
lambda
的箭头操作符)。 - 如果使用箭头函数(->)操作符,可以跳过
yield
关键字,如isWeekDayV1_1()
所示。 - 如果使用冒号(:)操作符,则需要使用
yield
关键字,如isWeekDayV1_2()
所示。 - 对于多个语句,使用大括号和
yield
关键字,如isWeekDayV2()
所示。 - 对于
enum
,我们可以跳过默认情况。如果有任何丢失的值没有在case
中处理,编译器将会报错。在所有其他表达式类型(int
,String
等)中,我们也必须提供默认情况。
1 | public class SwitchExpressions |
- yield 和 return
return
语句将控制权返回给方法或构造函数的调用者。yield
语句通过使封闭的switch
表达式产生指定的值来传递控制。
1 | SwitchExpression: |
SwitchExpression
试图找到一个正确的YieldStatement
,以便将控制转移到最内层的yield
目标。SwitchExpression
正常终止,表达式的值成为SwitchExpression
的值。- 如果表达式的求值由于某种原因突然结束,那么
yield
语句也会由于同样的原因突然结束。
更多新特性
JEP 343 – 打包工具(孵化)
在 JDK 8 中,一个名为javapackager
的工具作为 JavaFX 工具包的一部分发布。然而,随着 JDK 11 的发布,JavaFX 从 Java 中分离出来后,流行的javapackager
就不再可用了。
这个 JEP 基于javapackager
工具创建了一个简单的打包工具,该工具支持本地打包格式,为最终用户提供自然的安装体验。这些格式包括 Windows 上的 msi和exe, macOS 上的 pkg和dmg,以及Linux上的 deb和rpm。
该工具可以从命令行直接调用,也可以通过工具提供程序API以编程方式调用。
1 | $ jpackage --name myapp --input lib --main-jar main.jar |
JEP 345 – NUMA-Aware Memory Allocation for G1
在Numa(Non-Uniform Memory Access 非均匀内存访问)内存体系结构中,每个处理器接收少量的本地内存,但是其他核心被授予访问它的权限。
并行垃圾收集器(由-XX:+UseParallelGC
启用),多年来一直支持Numa,并提高了跨多个套接字运行单个 JVM 的配置的性能。
有了这个 JEP , G1垃圾收集器得到了增强,可以在 Linux OS 下更好地管理内存。
JEP 349 – JFR 事件流
该JEP为进程内和进程外程序公开了JDK运行情况记录数据,方便我们进行监控。
以前要使用这些数据,用户必须启动记录、停止记录、将内容转储到磁盘,然后解析记录文件。虽然对于程序分析比较友好,因为通常一次记录至少一分钟的时间,但是它不适用于实时监控。
该JEP对包 jdk.jfr.consumer
,jdk.jfr
模块进行了扩展,提供了异步订阅事件的功能。用户可以直接从磁盘存储库读取记录数据或流,而无需转储记录文件。
JEP 352 – 非易失性映射的字节缓冲区
这个 JEP 添加了一个新的特定于JDK的文件映射模式,这样FileChannel API
就可以用来创建引用 NVM(non-volatile memory 非易失性内存)的MappedByteBuffer
实例。NVM也被称为持久内存,用于永久存储数据。
当前对MappedByteBufer API
的更改意味着它支持允许直接内存更新所需的所有行为,并提供实现持久数据类型(例如块文件系统、日志记录日志、持久对象等)的更高级别Java客户端库所需的持久性保证。
JEP 363 – 删除并发标记清除(CMS)垃圾收集器
这个 JEP 的目的是删除掉在**Java 9 (JEP 291)**中被标记为deprecated
的CMS垃圾收集器。由于 CMS GC的代码难以理解维护,且两年内未有相关感兴趣人员进行维护和更新。
所以现在,CMS GC已经从Java 14中删除了。需要注意的是,CMS GC 在 Java 13之前都是可用的。
JEP 367 – 删除 Pack200 工具和相关 API
在java.util.jar
包中删除pack200
和unpack200
工具。Pack200 API
(Java SE 5.0中引入的JAR文件的压缩方案)在Java SE 11中他们已经被标记为deprecated
,不建议使用,且未来版本明确会删除。
JEP 370 – 外部内存访问API(孵化)
有了这个 JEP , Java 提供了一个 API 来允许 Java 程序安全有效地访问 Java 堆之外的外部内存。
目标是相同的 API 应该能够操作各种类型的外部内存(例如,本机内存、持久内存、托管堆内存等)。
无论操作的内存类型如何,API 都不应该破坏 JVM 的安全性。另外,在源代码中内存回收操作应该是显式的。