Java14 - 新特性
笔记
20020 年 3 月 17 日,国际知名的 OpenJDK 开源社区发布了 Java 编程语言环境的最新版本 OpenJDK14。Java 14 内容比 Java 12 和 13 加起来的还要多,也是超实用的 Java 版本新特性。
2022-02-08 @Young Kbt
# Java14概述
此版本包含的 JEP(Java/JDK Enhancement Proposals,JDK 增强提案)比 Java 12 和 13 加起来的还要多。总共 16 个新特性,包括两个孵化器模块、三个预览特性、两个弃用的功能以及两个删除的功能。
- 孵化器模块:将尚未定稿的 API 和工具先交给开发者使用,以获得反馈,并用这些反馈进一步改进 Java 平台的质量
- 预览特性:是规格已经成型、实现已经确定,但还未最终定稿的功能。它们出现在 Java 中的目的是收集在真实世界中使用后的反馈信息,促进这些功能的最终定稿。这些特性可能会随时改变,根据反馈结果,这些特性甚至可能会被移除,但通常所有预览特性最后都会在 Java 中固定下来
此次的发布与之前的 Java 11、12 和 13 一样,离不开 Open JDK 社区无数个人和组织的无私奉献。JDK 14 修正了 1986 个 JIRA 问题,其中 1458 个来自 Oracle 的员工,另外 528 个来自独立开发者和其他公司的开发者的提交。
上图的他是 JDK 1.2 和 5.0 版本的首席工程师,Java SE 6 的规范制定的负责人,还是 JDK 7,JDK 8 和 JDK 9 项目和规范的负责人,目前,他主要在 Open JDK 社区领导 JDK 项目。
Oracle JDK 和 Open JDK 区别如图:
Oracle JDK 不再免费提供。但是,你现在可以从包括 Oracle 在内的各种供应商获得免费的 Open JDK 发行版。
「语言必须发展,否则它们就有变得无关紧要的风险。」Brian Goetz(甲骨文公司)在 2019 年 11 月在 Devoxx 举行的「Java 语言期货」演讲中说。
尽管 Java 已经发展了 25 年,但仍然远远没有过时。下面,我们将研究 JDK 14 的创新。小步快跑,快速迭代。
JDK14 的下载地址:
https://www.oracle.com/java/technologies/javase-downloads.html
。
# instanceof的模式匹配(预览)
这个特性很有意思,因为它为更为通用的模式匹配打开了大门。模式匹配通过更为简便的语法基于一定的条件来抽取对象的组件,而 instanceof 刚好是这种情况,它先检查对象类型,然后再调用对象的方法或访问对象的字段。
有了该功能,可以减少 Java 程序中显式强制转换的数量,从而提高生产力,还能实现更精确、简洁的类型安全的代码。
Java 14 之前
if(obj instanceof String) {
String str = (String) obj; // 需要强转
Sysmtem.out.println(str + "手动强转");
}else {
str = .... // 报错,因为 str 的作用域是 if 里的
}
2
3
4
5
6
Java 14 新特性
if(!(obj instanceof String str)) { // str 已经强转
Sysmtem.out.println(str + "自动强转"); // 不再需要转换代码,实际发生了转换
}else {
str = .... // 报错,因为 str 的作用域是 if 里的
}
2
3
4
5
# 实用的NullPointerException
该特性改进了 NullPointerException
的可读性,能更准确地给出 null 变量的信息。
该特性可以帮助开发者提高生产力,以及改进各种开发工具和调试工具的质量。一个目标是减少开发人员的困惑和担忧。
相信很多 Java 程序员都一样对 null 和 NPE 深恶痛绝,因为他确实会带来各种各样的问题(来自《Java 8 实战》)。如:
- 它是错误之源。
NullPointerException
是目前 Java 程序开发中最典型的异常。它会使你的代码膨胀 - 它让你的代码充斥着深度嵌套的 null 检查,代码的可读性糟糕透顶
- 它自身是毫无意义的。 null 自身没有任何的语义,尤其是它代表的是在静态类型语言中以一种错误的方式对缺失变量值的建模
- 它破坏了 Java 的哲学。Java 一直试图避免让程序员意识到指针的存在,唯一的例外是:null 指针
- 它在 Java 的类型系统上开了个口子。null 并不属于任何类型,这意味着它可以被赋值给任意引用类型的变量。这会导致问题,原因是当这个变量被传递到系统中的另一个部分后,你将无法获知这个 null 变量最初赋值到底是什么类型
# 其他语言如何解决NPE问题
在 Groovy 中使用安全导航操作符(Safe Navigation Operator)可以访问可能为 null 的变量:
def carInsuranceName = person?.car?.insurance?.name
1在 Haskell 和 Scala 也有类似的替代品,如 Haskell 中的 Maybe 类型、Scala 中的 Option[A]。Option[A] 是一个类型为 A 的可选值的容器
在 Kotlin 中,其类型系统严格区分一个引用可以容纳 null 还是不能容纳。也就是说,一个变量是否可空必须显示声明,对于可空变量,在访问其成员时必须做空处理,否则无法编译通过:
var a: String = "abc" a = null // 编译错误 // 如果允许为空,可以声明一个可空字符串,写作 String? var b: String? = "abc" // String? 表示该 String 类型变量可为空 b = null // 编译通过
1
2
3
4
5
6
# Java做了哪些努力
首先在 Java 8 中提供了 Optional。
- Optional 在可能为 null 的对象上做了一层封装,强制你思考值不存在的情况,这样就能避免潜在的空指针异常
- 关于 Optional 的用法,不是本文的重点。在日常开发中经常结合 Stream 一起使用 Optional,还是比较好用的
另外一个值得一提的就是最近(2020 年 03 月 17 日)发布的 JDK 14 中对于 NPE 有了一个增强。那就是 JEP 358: Helpful NullPointerExceptions
- 该特性可以更好地提示哪个地方出现的空指针,需要通过
-XX:+ShowCodeDetailsInExceptionMessages
开启 - 在未来的版本中,这个特性可能会默认启用
- 这个增强特性不仅适用于方法调用,只要会导致 NullPointerException 的地方也都适用,包括字段的访问、数组的访问和赋值
如开启 -XX:+ShowCodeDetailsInExceptionMessages
参数后的报错内容(明确知道哪个方法出现 null 异常):
Exception in thread "main" java.lang.NullPointerException: Cannot invoke
"com.youngkbt.feature.Account.withdraw(double)" because the return value of
"com.youngkbt.feature.Customer.getAccount()" is null
2
3
# Record(预览特性)
JEP 359:Record(预览特性)。
# 官方吐槽
早在 2019 年 2 月份,Java 语言架构师 Brian Goetz,曾经写过一篇文章,详尽的说明了并吐槽了 Java 语言,他和很多程序员一样抱怨「Java 太啰嗦」或有太多的「繁文缛节」,他提到:开发人员想要创建纯数据载体类(plain data carriers)通常都必须编写大量低价值、重复的、容易出错的代码。如:构造函数、getter/setter
、equals()
、hashCode()
以及 toString()
等。
以至于很多人选择使用 IDE 的功能来自动生成这些代码。还有一些开发会选择使用一些第三方类库,如 Lombok 等来生成这些方法,从而会导致了令人吃惊的表现(surprising behavior)和糟糕的可调试性(poor debuggability)。
我们有时候需要编写许多低价值的重复代码来实现一个简单的数据载体类:构造函数,访问器,equals()
,hashCode()
,toString()
等。为了避免这种重复代码,Java 14 推出 Record。
# Record出现
我们有时候需要编写许多低价值的重复代码来实现一个简单的数据载体类:构造函数,访问器,equals()
,hashCode()
,toString()
等。为了避免这种重复代码,Java 14 推出 Record。
Java14 也许最令人兴奋,同时也是最令人惊讶的创新就是:Record 类型的引入 。
使用 Record 来减少类声明语法,效果类似 lombok 的 @Data 注解,Kotlin 中的 Data Class。它们的共同点是类的部分或全部状态可以直接在类头中描述,并且这个类中只包含了纯数据而已。
该预览特性提供了一种更为紧凑的语法来声明类。值得一提的是,该特性可以大幅减少定义类似数据类型时所需的样板代码。
代码示例
public record Person(String name,Person partner) {
}
2
当你用 Record 声明一个类时,该类将自动拥有以下功能:
- 获取成员变量的简单方法,以上面代码为例
name()
和partner()
。注意区别 于我们平常 getter 的写法 - 一个
equals
方法的实现,执行比较时会比较该类的所有成员属性 - 重写
equals
当然要重写hashCode
- 一个可以打印该类所有成员属性的
toString
方法 - 请注意只会有一个构造方法
代码测试
public void test1(){
//测试构造器
Person p1 = new Person("罗密欧",new Person("zhuliye",null));
//测试 toString()
System.out.println(p1);
//测试 equals()
Person p2 = new Person("罗密欧",new Person("zhuliye",null));
System.out.println(p1.equals(p2));
//测试 hashCode() 和 equals()
HashSet<Person> set = new HashSet<>();
set.add(p1);
set.add(p2);
for (Person person : set) {
System.out.println(person);
}
//测试 name() 和 partner():类似于 getName() 和 getPartner()
System.out.println(p1.name());
System.out.println(p1.partner());
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
和枚举类型一样,记录也是类的一种受限形式。作为回报,记录对象在简洁性方面提供了显著的好处:
- Enum interface
- 还可以在 Record 声明的类中定义静态字段、静态方法、构造器或实例方法
- 不能在 Record 声明的类中定义实例字段;类不能声明为 abstract;不能声明显式的父类等
- Record 类没有 setter 方法,只有 getter 方法,给参数赋值是在 new 时给构造器传参,如果想要 setter 方法,需要手动生成
代码示例
public record Person(String name,Person partner) {
// 还可以声明静态的属性、静态的方法、构造器、实例方法
public static String nation;
public static String showNation(){
return nation;
}
public Person(String name){
this(name,null);
}
public String getNameInUpperCase(){
return name.toUpperCase();
}
// 不可以声明非静态的属性
//private int id; // 报错
}
//不可以将 record 定义的类声明为 abstract 的,因为编译后,Record 类是 final,无法被继承
//abstract record Order(){
//}
//不可以给 record 定义的类声明显式的父类(非 Record 类),因为编译后 Record 已经有父类,不能多继承
//record Order() extends Thread{
//}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
为了在 Java 14 中引入这种新类型,需要在 Java.lang.Class
对象中添加如下两个新方法:
RecordComponent[] getRecordComponents()
boolean isRecord()
上面两个方法是为将 Record 引入反射的大家庭里,即可以通过反射获取 Record 类的信息。
# Switch表达式
JEP 361:Switch 表达式。
Switch 表达式是 JDK 12 和 JDK 13 中的预览特性,现在是正式特性了。
该特性规定,Switch 可以当作语句使用,也可以当作表达式使用。
这可以简化日常的编码方式,也为本版本中预览的模式匹配(JEP 305)特性打下了基础。
具体情况:
- 使用
->
来替代以前的: break
;另外还提供了yield
来在 Block 中返回值
代码示例
public class Feature04 {
// JDK12 之前的用法
@Test
public void test1() {
Week day = Week.FRIDAY;
switch (day) {
case MONDAY:
case TUESDAY:
case WEDNESDAY:
System.out.println(1);
break;
case THURSDAY:
System.out.println(2);
break;
case FRIDAY:
case SATURDAY:
System.out.println(3);
break;
case SUNDAY:
System.out.println(4);
break;
default:
throw new IllegalStateException("What day is today?" + day);
}
}
// JDK12 新特性:引用 Switch 表达式
@Test
public void test2(){
Week day = Week.FRIDAY;
switch (day){
case MONDAY,TUESDAY,WEDNESDAY -> System.out.println(1);
case THURSDAY -> System.out.println(2);
case FRIDAY,SATURDAY -> System.out.println(3);
case SUNDAY -> System.out.println(4);
default -> throw new IllegalStateException("What day is today?" + day);
}
// 使用变量接收 Switch 表达式的值
int num = switch (day) {
case MONDAY, TUESDAY, WEDNESDAY -> 1;
case THURSDAY -> 2;
case FRIDAY, SATURDAY -> 3;
case SUNDAY -> 4;
default -> throw new IllegalStateException("What day is today?" + day);
};
System.out.println(num);
}
// JDK13 新特性:引用了 yield 关键字,用于返回指定的数据,结束 Switch 结构
@Test
public void test3(){
String x = "3";
int num = switch (x){
case "1" -> 1;
case "2" -> 2;
case "3" -> 3;
default -> {
System.out.println("default...");
yield 4;
}
};
System.out.println(x);
}
@Test
public void test4(){
String x = "3";
int num = switch (x){
case "1":yield 1;
case "2":yield 2;
case "3":yield 3;
default: yield 4;
};
System.out.println(num);
}
}
enum Week {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# 文本块(预览第二版)
JEP 368:文本块(预览第二版)。
JDK13 引入的 Text Blocks 进行第二轮 Preview,JDK14 的版本主要增加了两个 escape、sequences,分别是 \
(取消换行) 与 \s
(相当于空格)。
现实问题:
- 在 Java 中,通常需要使用 String 类型表达 HTML,XML,SQL 或 JSON 等格式的字符串,在进行字符串赋值时需要进行转义和连接操作,然后才能编译该代码,这种表达方式难以阅读并且难以维护
目标:
- 简化跨越多行的字符串,避免对换行等特殊字符进行转义,简化编写 Java 程序
- 增强 Java 程序中用字符串表示的其他语言的代码的可读性
- 解析新的转义序列
代码示例
public void test2(){
// JDK 12 之前的写法
String sql = "SELECT id,NAME,email\n" +
"FROM customers\n" +
"WHERE id > 4\n" +
"ORDER BY email DESC";
// JDK13 中的新特性:
String sql1 = """
SELECT id,NAME,email
FROM customers
WHERE id > 4
ORDER BY email DESC
""";
// JDK14 新特性
// \:取消换行操作
// \s:表示一个空格
String sql2 = """
SELECT id,NAME,email\ // 等价于 SELECT id,NAME,emailFROM customers
FROM customers\s\ // 等价于 FROM customers WHERE id > 4
WHERE id > 4 \ // 等价于 WHERE id > 4 ORDER BY email DESC
ORDER BY email DESC
""";
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 弃用ParallelScavenge和SerialOld GC组合
JEP 366:弃用 ParallelScavenge 和 SerialOld GC 组合。
由于维护和兼容性测试的成本,在 JDK 8 时将 Serial + CMS、ParNew + Serial Old 这两个组合声明为废弃(JEP 173),并在 JDK 9 中完全取消了这些组合的支持(JEP214)。ParallelScavenge + SerialOld GC 的 GC 组合要被标记为 Deprecate 了。
JDK 官方给出将这个 GC 组合标记为 Deprecate 的理由是:这个 GC 组合需要大量的代码维护工作,并且,这个 GC 组合很少被使用。因为它的使用场景应该是一个很大的 Young 区配合一个很小的 Old 区,这样的话,Old 区用 SerialOld GC 去收集时停顿时间我们才能勉强接受。
废弃了 Parallel Young Generation GC 与 SerialOld GC 的组合(-XX:+UseParallelGC
与 XX:-UseParallelOldGC
配合开启),现在使用 -XX:+UseParallelGC
和 -XX:-UseParallelOldGC
或者 -XX:-UseParallelOldGC
都会出现告警如下:
Java HotSpot(TM) 64-Bit Server VM warning: Option
UseParallelOldGC was deprecated in version 14.0 and will likely
be removed in a future release.
2
3
# 删除CMS垃圾回收器
JEP 363:删除 CMS 垃圾回收器。
该来的总会来,自从 G1(基于 Region 分代)横空出世后,CMS 在 JDK9 中就被标记为 Deprecate 了(JEP 291: Deprecate the Concurrent Mark Sweep (CMS) Garbage Collector)。
CMS 的弊端:
- 会产生内存碎片,导致并发清除后,用户线程可用的空间不足
- 既然强调了并发(Concurrent),CMS 收集器对 CPU 资源非常敏感
- CMS 收集器无法处理浮动垃圾
上述的这些问题,尤其是碎片化问题,给你的 JVM 实例就像埋了一颗炸弹。说不定哪次就在你的业务高峰期来一次 Full GC。当 CMS 停止工作时,会把 Serial Old GC 作为备选方案,而 Serial Old GC 是 JVM 中性能最差的垃圾回收方式,停顿个几秒钟,上十秒都有可能。
移除了 CMS 垃圾收集器,如果在 JDK14 中使用 -XX:+UseConcMarkSweepGC
的话,JVM 不会报错,只是给出一个 Warning 信息。
Java HotSpot(TM) 64-Bit Server VM warning: Ignoring option UseConcMarkSweepGC; support was removed in 14.0
现在 G1 回收器已成为默认回收器好几年了。
我们还看到了引入了两个新的收集器:ZGC(JDK11 出现)和 Shenandoah(Open JDK12)。
- 主打特点:低停顿时间
Shenandoah 开发团队在实际应用中的测试数据
收集器 | 运行时间 | 总停顿 | 最大停顿 | 平均停顿 |
---|---|---|---|---|
Shenandoah | 387.602s | 320ms | 89.79ms | 53.01ms |
G1 | 312.052s | 11.7s | 1.24s | 450.12ms |
CMS | 285.264s | 12.78s | 4.39s | 852.26ms |
Parallel Scavenge | 260.092s | 6.59s | 3.04s | 823.75ms |
# ZGC on MacOS和Windows
令人震惊、革命性的 ZGC。
ZGC 与 Shenandoah 目标高度相似,在尽可能对吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟。
《深入理解 Java 虚拟机》一书中这样定义 ZGC:ZGC 收集器是一款基于 Region 内存布局的,(暂时)不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-压缩算法的,以低延迟为首要目标的一款垃圾收集器。
虽然 ZGC 还在试验状态,没有完成所有特性,但此时性能已经相当亮眼,用「令人震惊、革命性」来形容,不为过。未来将在服务端、大内存、低延迟应用的首选垃圾收集器。
- JEP 364:ZGC 应用在 MacOS 上
- JEP 365:ZGC 应用在 Windows 上
JDK14 之前,ZGC 仅 Linux 才支持。
尽管许多使用 ZGC 的用户都使用类 Linux 的环境,但在 Windows 和 MacOS 上,人们也需要 ZGC 进行开发部署和测试。许多桌面应用也可以从 ZGC 中受益。因此,ZGC 特性被移植到了 Windows 和 MacOS 上。
现在 Mac 或 Windows 上也能使用 ZGC 了,示例如下:-XX:+UnlockExperimentalVMOptions
和 -XX:+UseZGC
。
# 其它新特性
# 打包工具(孵化器模块)
这个孵化器工具为开发者带来了一种打包 Java 应用的方式,目的在于创建一个简单的打包工具,可以用于构建 exe、pkg、dmg、deb、rpm 格式的安装文件。
JDK14 引入了 jdk.incubator.jpackage.jmod
,它基于 JavaFX javapackager tool 构建。
# G1的NUMA-Aware的内存分配
该功能改进了 G1 垃圾回收器在非一致内存访问(NUMA)系统上的整体性能。
NUMA 就是非统一内存访问架构(英语:non-uniform memory access,简称 NUMA),是一种为多处理器的电脑设计的内存架构,内存访问时间取决于内存相对于处理器的位置。
# JFR事件流
JEE 349:JFR 事件流。
Java 为了更方便的了解运行的 JVM 情况,在之前的 JDK11 版本中引入了 JFR 特性,即 JDK Flight Recorder。但是使用不太灵活。虽然 JVM 通过 JFR 暴露了超过 500 项数据,但是其中大部分数据只能通过解析 JFR 日志文件才能获取得到,而不是实时获取。用户想要使用 JFR 的数据的话,用户必须先开启 JFR 进行记录,然后停止记录,再将飞行记录的数据 Dump 到磁盘上,然后分析这个记录文件。
举例:
jcmd <PID> JFR.start name=test duration=60s settings=template.jfc filename=output.jfr
新特性中,可以公开 JDK Flight Recorder(JFR)的数据,用于 持续监视,从而简化各种工具和应用程序对 JFR 数据的访问。
# 非易失性映射字节缓冲区
在 JEP 352 中,对 FileChannel API 进行了扩展,以允许创建 MappedByteBuffer 实例。
与易失性存储器(RAM)不同,它们在非易失性数据存储(NVM,非易失性存储器)上工作。但是,目标平台是 Linux x64。
非易失性内存能够持久保持数据,因此可以利用该特性来改进性能。
# 最后三个新特性
JEP 370:外部内存访问 API
JEP 362:弃用 Solaris 和 SPARC 的移植
JEP 367:删除 Pack200 工具和 API
上面列出的是大方面的特性,除此之外还有一些 API 的更新及废弃,主要见 JDK 14 Release Notes。这里不再赘述。
# 写在最后
JDK 14 性能提升,但 JDK 8 仍是最强王者。
在两个长期支持的版本 JDK 8 和 JDK 11 中,相比之下肯定是 JDK 11 的新特性更多一些,但是并没有真的非升不可的新特性。