Java17已经于2021年3月16日发布,按照Oracle的规则,这是一个LTS(长期支持版),本篇学习并记录下该版本的新特性。

密封类(Sealed Classes) JEP409

这是 Java 17 最重要的语法特性。它允许类或接口的作者控制哪些代码可以实现或继承它们。这为 代数数据类型 (ADT) 提供了基础。

例如:

1
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
package cn.net.dev;

// Shape只能被指定的类继承或者实现,这里只能被Circle和Square实现
sealed interface Shape permits Circle, Square {
}
// 不能再被继承,必须使用final
final class Circle implements Shape {
private final double radius;
public Circle(double radius) {
this.radius = radius;
}
public double radius() {
return radius;
}
}
// non-sealed,取消sealed限制,可以被任意类继承,所以不需要final
non-sealed class Square implements Shape {
private final double side;
public Square(double side) {
this.side = side;
}
public double side() {
return side;
}
}
public class SealedDemo {
public static void main(String[] args) {
Shape shape = new Circle(5.0);
System.out.println("面积 " + area(shape));
}
static double area(Shape shape) {
if (shape instanceof Circle c) {
return Math.PI * c.radius() * c.radius();
} else if (shape instanceof Square s) {
return s.side() * s.side();
} else {
throw new IllegalStateException("Unknown shape");
}
}
}

记录类(Records) JEP 359、JEP 384、JEP 395

经过 JEP 359JEP 384JEP 395三个版本的迭代,在Java16发布了证书版本,不过我只用LTS的版本,所以就将它向上归类到Java17的新特性

记录类的定义非常灵活,我们可以在单独文件中定义,也可以在类内部定义,甚至在函数内部定义。记录类的使用和普通类无异,使用 new 创建即可。它的特点如下:

  • 它是一个final类;
  • 不能继承其他类(包括记录类)
  • 所有字段都是final的,一旦创建就不可以修改。
  • 内置实现了构造函数,构造参数就是所有字段
  • 内置实现了getter方法。
  • 内置实现了equals()hashCode()toString()方法

使用该类型,可以极大的简化不可变数据传输对象的编写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package cn.net.dev;

record User(Long id, String name, String email) {
}

public class RecordDemo {
public static void main(String[] args) {
User u1 = new User(1L, "zhangsan", "zhangsan@mail.com");
User u2 = new User(1L, "lisi", "lisi@mail.com");

System.out.println(u1.name());
System.out.println(u1);
System.out.println(u1.equals(u2));
}
}

文本块 (Text Blocks) JEP 378

文本块是在Java 15正式发布的,它是一种多行字符串字面量,它避免了大多数转义序列的需要,以可预测的方式自动格式化字符串,并在需要时给予开发者对格式的控制。

1
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
package cn.net.dev;

public class TextBlockDemo {
public static void main(String[] args) {
String htmlBefore = "<html>\n" +
" <head>\n" +
" <title>Hello Java</title>\n" +
" </head>\n" +
" <body>\n" +
" <div>111</div>\n" +
" </body>\n" +
" </html>";

String html = """
<html>
<head>
<title>Hello Java</title>
</head>
<body>
<div>111</div>
</body>
</html>
""";
String json = """
{
"name":"zhangsan",
"age":20,
"email":"zhangsan@mail.com"
}
""";
String sql = """
SELECT id, name FROM users
WHERE status = 'ACTIVE'
ORDER BY created_at DESC;
""";
System.out.println(json);
System.out.println(sql);
}
}

如果需要转义,变量替换。可以使用如下方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
String name = "lisi";
String msg = """
Hello My Name is $name
""".replace("$name", name);
System.out.println(msg);

String msg1 = String.format("""
Hello My Name is %s
""", name);
System.out.println(msg1);

String msg2 = """
Hello My Name is %s
""".formatted(name);

这三种方法都可以,这样就可以避免字符串拼接(”””字符串拼接的用法与普通字符串拼接一致)。

"""里面,可以使用\来作为<line-terminator>(类似于LINUX中的用法),使用\s转译为单个空格。

1
2
3
4
5
6
7
String text = """
Hello, \
My Name is lisi,\
I love Java.\
""";
System.out.println(text);
// 输出为:Hello, My Name is lisi,I love Java.

和字符串相关,就避免不了转义,三个连续的 " 字符至少需要有一个 " 进行转义,以避免模仿结束定界符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
String text1 = """
String text1 = \"""
Hello Java
\"""
""";
System.out.println(text1);

System.out.println("""
1 "
2 ""
3 ""\"
4 ""\""
5 ""\"""
6 ""\"""\"
7 ""\"""\""
8 ""\"""\"""
9 ""\"""\"""\"
10 ""\"""\"""\""
11 ""\"""\"""\"""
12 ""\"""\"""\"""\"
""");

instanceof 模式匹配 (Pattern Matching for instanceof)

消除了显式的强制类型转换,代码更安全、更简洁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package cn.net.dev;

public class InstanceofDemo {
public static void main(String[] args) {
Object obj = "Hello Java 17";
if (obj instanceof String str) {
String string = (String) obj;
if (string.length() > 5) {
System.out.println("这是一个字符串,并且长度大于5");
}
}
// Java 17
if (obj instanceof String str && str.length() > 5) {
System.out.println("这是一个字符串,并且长度大于5");
}
}
}

伪随机数生成器增强 JEP411

引入了 RandomGenerator 接口,提供了更丰富的随机数算法(如 LXM 族算法)。

1
2
3
4
5
6
7
8
9
10
11
package cn.net.dev;

import java.util.random.RandomGenerator;
import java.util.random.RandomGeneratorFactory;

public class RandomDemo {
public static void main(String[] args) {
RandomGenerator g = RandomGeneratorFactory.of("L64X128MixRandom").create();
System.out.println("随机数: " + g.nextInt(8));
}
}

Switch表达式

这是Java14正式发布的功能,也是在简化代码开发,我觉得还是有必要使用一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package cn.net.dev;

public class SwitchExpDemo {
public static void main(String[] args) {
String day = "MONDAY";
int numLetters = switch (day) {
case "MONDAY", "FRIDAY", "SUNDAY" -> 6;
case "TUESDAY" -> 7;
case "THURSDAY", "SATURDAY" -> 8;
case "WEDNESDAY" -> 9;
default -> throw new IllegalArgumentException("Invalid day " + day);
};
System.out.println("字母数 :" + numLetters);
}
}

Teeing Collector(API增强)

Stream API 的增强,允许在一个流中同时执行两个不同的收集操作,并合并结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package cn.net.dev;

import java.util.stream.Stream;

import static java.util.stream.Collectors.*;

public class TeeingDemo {
public static void main(String[] args) {
var result = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).collect(
teeing(
// 求和
summingInt(Integer::intValue),
// 计数
counting(),
// 合并逻辑,求平均
(sum, count) -> sum / (double) count
)
);
System.out.println(result);
}
}

NullPointerExecption提示更友好

这个是在Java 14正式发布的,虽然也算不上新特性。

1
2
3
4
5
6
A a = new A();
a = null;
a.call();

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "cn.net.dev.A.call()" because "a" is null
at cn.net.dev.NpeDemo.main(NpeDemo.java:14)

我把JDK版本换到Java11

1
2
Exception in thread "main" java.lang.NullPointerException
at cn.net.dev.NpeDemo.main(NpeDemo.java:11)

ZGC

大名鼎鼎的ZGC也是在Java 15终于正式发布了。它是一个可伸缩、低延迟的垃圾回收器,目标是无论堆多大,停顿时间都不超过 10ms。在Java启动参数中,添加-XX:+UseZGC即可。

1
java -XX:+UseZGC -Xms8g -Xmx8g -jar app.jar

ZGC的设计哲学是自适应,所以不需要太多的JVM参数,比较常用的如下所示:

参数 说明 建议
-XX:ParallelGCThreads=n 并行GC阶段使用的线程数 默认根据CPU的核心数计算,通常无需手动指定。
-XX:ConcGCThreads=n 并发GC阶段使用的线程数 如果发现GC速度赶不上分配速度,可以适当增加
-Xlog:gc* 开启详细的GC日志 诊断性能瓶颈的必备工具。
-XX:SoftMaxHeapSize=n 软最大堆限制 告诉JVM尽量保持堆在多少以下,超出后会更积极的回收。

一般只需要开启日志即可。

1
java -XX:+UseZGC -Xlog:gc*:file=gc.log:time,level,tags -jar app.jar

gc.log 中,你会看到 ZGC 的三个核心阶段:

  1. Pause Mark Start:停顿(极短)。
  2. Concurrent Mark:并发标记(不影响业务)。
  3. Pause Mark End:停顿(极短)。
  4. Concurrent Relocate:并发迁移(不影响业务)。

2024年1月1日更新:Java21的分代ZGC更靠谱,更加好用。推荐升级到Java 21

新类层次结构分析实现 (New Class Hierarchy Analysis Implementation)

它属于JDK-8266074,这个不是JEP(JDK Enhancement Proposal),但也了解一下。

CHA 是什么?

CHA (Class Hierarchy Analysis,类层次结构分析) 是 JIT 编译器(C1/C2)用来优化代码的一种编译器技术。

  • 核心目的:判断一个虚方法(Virtual Method)调用是否只有一个实际的实现类(即“单态性”)。
  • 作用:如果编译器发现某个方法没有被子类重写,它就会大胆地进行 内联(Inlining),直接把方法体代码考过来,避免昂贵的虚方法表查询。

在 Java 17 之前,CHA 的实现逻辑比较古老且受限。JDK-8266074 引入了一套全新的、基于 Vtable(虚函数表) 的分析机制。

  • 更精准的判断:新实现能更敏锐地处理抽象方法默认方法(Default Methods)
  • 更好的内联决策:它能发现更多可以被内联的场景,尤其是在复杂的接口继承体系中。
  • 默认开启:在 Java 17 中,这个新特性是默认开启的。

虚应用的自动清理

同上,不是JEP,属于JDK-8071507。在 Java 17 中,PhantomReference(虚引用)在对象不可达时会自动被 GC 清理,就像弱引用和软引用一样。减少了手动清理虚引用(以前看书疯狂JAVA讲义,书里翻译的是幻像引用,同一个东西,为了和强、弱一致,用虚更好一点)队列的负担,降低了内存泄漏风险。

还有一些swingjava.net的新特性,就不需要了解了,用到再说。还有一个Ideal Graph Visualizer的现代化也可以了解下,具体看这个介绍即可。