Java 21 虚拟线程外其他新特性
迁移完所有的 WordPress 日志到 Hugo 之后, 终于有时间真正继承学习相关的新技术. Java 21 是于 2023 年 9 月份释放出来的 LTS 版本, 目前主要在用该版. Java 25 LTS 版本已发布, 按正常节奏应该要切换到该版本.
随着 AI 在编程界的花式表演, 所宣传的似乎就是要扑灭他人的学习热情, 编程方面越小白越好, 只要能写好小作文就行了. AI 当然还是要用, 但我对以往多少年传统的学习方式并不感到白花了心血. 告诉 AI 的一个课题 AI 确实能写出一篇漂亮, 规整的博客文章, 但其中有没有胡说八道, 只有试了才知道, 即使生成的文章无误, 也必须实践一遍才有更多更深的斩获.
如果没有相关的技术储备, 每次与 AI 互动的时候都要告诉它尽量用 Java 21 新特性, 因为新引入的特性基本能实现得更简洁, 高效, 估计 AI 才不那么在乎这些, 写出适于人阅读的代码恐怕不是 AI 的首要关注.
还是老办法, 关于 Java 某一版本新特性从两个链接出发
把那些关键的新特性列出来就是
- 430: String Templates (Preview) *
- 431: Sequenced Collections *
- 439: Generational ZGC *
- 440: Record Patterns *
- 441: Pattern Matching for switch *
- 442: Foreign Function & Memory API (Third Preview)
- 443: Unnamed Patterns and Variables (Preview)
- 444: Virtual Threads
- 445: Unnamed Classes and Instance Main Methods (Preview)
- 446: Scoped Values (Preview)
- 448: Vector API (Sixth Incubator)
- 449: Deprecate the Windows 32-bit x86 Port for Removal
- 451: Prepare to Disallow the Dynamic Loading of Agents
- 452: Key Encapsulation Mechanism API
- 453: Structured Concurrency (Preview)
其中 444 虚拟线程最 Java 21 最受关注的新特性, 所以单独写过一篇日志 Java 21 虚拟线程外其他新特性, 在另一篇 Java 19, 20 新特性学习 也简单介绍了非正式的 440 Record Patterns, 446 Scoped Values, 和 453 Structured Concurrency.
在 IntelliJ IDEA 中标识 Java 21 的 Language level: Record patterns, pattern matching for watching, Java 21 Prview:
String templates, unnamed classes and instance main methods, etc.. 奇怪的是为什么不提 Virtual Threads
本文主要学习的是正式的新特性 431 Sequenced Collections, 439 Generational ZGC, 440 Record Patterns, 441 Pattern Matching for switch, 并且提前了解一下 430 String Templates.
430: String Templates
这是一个预备学习, 该特性直到 JDK 26 也没见到定稿, 也有可能终将放弃. 大概来看一下它要解决什么问题.简单来讲就是它想要实现其他编程语言中的字符串插值功能,
即 String interpolation. Java 不能满足于用加号, StringBuilder, String.format(), str.formatted(), 或 MessageFormat() 等方式来构造字符串,
希望能有像 Python F-String 功能, 如 f"{x} plus {y} equals {x + y}".
String interpolaton 在不少流行语言中都支持, 如 C#, Python, Scala, Groovy, Kotlin, JavaScript, Ruby, Swift 等.
String Teamplates 有三个重要 API, 它们分别是
- java.lang.StringTemplate.STR;
- java.lang.StringTemplate.RAW;
- java.util.FormatProcessor.FMT;
其中第一个是可以直接在源码中用 STR, 像 Integer 一样会被自动引入. 其他两个要显示用 import 引入. 看它们的定义
1Processor<String, RuntimeException> STR = StringTemplate::interpolate;
2Processor<StringTemplate, RuntimeException> RAW = st -> st;
3public static final FormatProcessor FMT = FormatProcessor.create(Locale.ROOT);
综合看它们的各种用法
1String name = "Joan";
2String info = STR."My name is \{name}, age \{20 + 5}, fullname: \{"DOE, " + name.toUpperCase()}";
3System.out.println("1\n" + info);
4
5StringTemplate st = RAW."My name is \{name}, age \{20 + 5}, fullname: \{"DOE, " + name.toUpperCase()}";
6System.out.println("2\n" + STR.process(st));
7System.out.println("3\n" + st.toString());
8
9Integer a = 10;
10
11String msg = STR."file exists: \{
12 new File("test.").exists() ?
13 "yes" : "no"
14 }";
15System.out.println("4\n" + msg);
16
17String[] names = {"John", "Jane", "Doe"};
18String html = STR."""
19 <html>
20 <body>
21 <h1>Hello, \{names[1]}!</h1>
22 <p>Welcome to the Java 21 world.</p>
23 </body>
24 """;
25System.out.println("5\n" + html);
26
27Object[] data = {"Alice", 30, "Engineer"};
28String formated = FMT."""
29 Name age profession
30 %-8s\{data[0]} %-5d\{data[1]} %-12s\{data[2]}
31 """;
32System.out.println("6\n" + formated);
执行后输出为
11
2My name is Joan, age 25, fullname: DOE, JOAN
32
4My name is Joan, age 25, fullname: DOE, JOAN
53
6StringTemplate{ fragments = [ "My name is ", ", age ", ", fullname: ", "" ], values = [Joan, 25, DOE, JOAN] }
74
8file exists: no
95
10<html>
11 <body>
12 <h1>Hello, Jane!</h1>
13 <p>Welcome to the Java 21 world.</p>
14 </body>
15
166
17Name age profession
18Alice 30 Engineer
很容易对照理解 STR, RAW, 和 FMT 的用法, 用 \{} 在单行或多行字符串中插入各种行式表达式的值. \{} 即使在单行的字符串中也能使用多行形式的表达式.
STR 好比 Python 的 f-string, 只是 Python 的 f-string 兼具 Java 的 FMT 的功能, 而 RAW 就像是 Python t-string.
像 STR."", RAW."", 和 FMT."" 的形式似是一种特殊的语法数糖, 如果反编译如下的代码
| |
字节码为
| |
StringTemplate 是一个接口, 我们看它定义的静态方法

我们能够手动通过指定 fragments 和 values 来构造一个 StringTemplate, StringTemplate.Processor 是一个函数式接口, 它定义的唯一抽象方法是
1R process(StringTemplate stringTemplate) throws E;
它是一个泛型方法, 返回值可为任意类型, 所以这里就可以像 Python 的 t-string 可定制了, 比如类似下面的方式
1 StringTemplate st = null; // create a StringTemplate instance
2 List<Object> ret = st.process(s -> {
3 List<Object> result = new ArrayList<>();
4 result.add(s.fragments()); // process fragments
5 result.add(s.values()); // process values
6 return result;
7 });
从 StringTemplate 中可得到任意的数据类型.
最后使用 String Template 时要注意安全, 小心像 SQL Injection 那样被注入了恶意代码.
431: Sequenced Collections
为了某些内部维持有顺序的集合操作便利, 在集合层次关系中插入了几个新的接口 SequencedCollection, SequencedSet, 和 SequencedMap
1interface SequencedCollection<E> extends Collection<E> {
2 // new method
3 SequencedCollection<E> reversed();
4 // methods promoted from Deque
5 void addFirst(E);
6 void addLast(E);
7 E getFirst();
8 E getLast();
9 E removeFirst();
10 E removeLast();
11}
1interface SequencedSet<E> extends Set<E>, SequencedCollection<E> {
2 SequencedSet<E> reversed(); // covariant override
3}
1interface SequencedMap<K,V> extends Map<K,V> {
2 // new methods
3 SequencedMap<K,V> reversed();
4 SequencedSet<K> sequencedKeySet();
5 SequencedCollection<V> sequencedValues();
6 SequencedSet<Entry<K,V>> sequencedEntrySet();
7 V putFirst(K, V);
8 V putLast(K, V);
9 // methods promoted from NavigableMap
10 Entry<K, V> firstEntry();
11 Entry<K, V> lastEntry();
12 Entry<K, V> pollFirstEntry();
13 Entry<K, V> pollLastEntry();
14}
它们在类层次中的位置是

针对相应的集合类型, 某些操作更直截了当了, 如对 List 取首尾元素不再用 list.get(0)(或 list.iterator().next()),
和 list.get(list.size() - 1), 用新的 API list.getFirst() 和 list.getLast()
439: Generational ZGC - 分代式 ZGC
自 G1 GC 在 Java 7 Update 4 正式可以, Java 9 才开始作为默认 GC, 而作为下一代的 ZGC 在 Java 15 可正式使用, 但在 Java 25 中仍默认使用 G1 GC. GC 算法一直在演化, 与之相关的概念有串行, 并行, 标识清除, 分代, 分 Region, 在时间与空间中寻找平衡. 原来时常感受到 STW(Stop The World), 现代的 GC 算法更追求的是低延迟, 现在有了 G1(Garbage First), ZGC 新垃圾回收算法
不知道为什么叫做 ZGC, 只找到了 JEP 333 的标题是 JEP 333: ZGC: A Scalable Low-Latency Garbage Collector (Experimental)
下面简单对比 G1 和 ZGC
- G1 仍用分代收集, Young Generation 和 Old Generation, 但它划分了若干 Region, 进行可预测暂停, 渐进式回收. 支持的堆大小在 4-64GB 之间, 可接受暂停时间在 100-200ms 之间. 比 ZGC 拥有更高的吞量
- ZGC 几乎所有工作都并发进行, 使用染色指针(Colored Pointers), 读屏障(Load Barriers) 技术, 支持更短的暂停时间(< 10ms), 支持超大堆内存(16TB). 需要足够的 CPU 资源.
ZGC 在 Java 21 之前是非分代收集, 如果要启用 ZGC 和分代式 ZGC, 要使用启动参数
1-XX:+UseZGC -XX:+ZGenerational
Java 21 在选用 ZGC 时, 默认仍然是不分代的, 要到 Java 23 选择 ZGC 时默认使用分代式收集, 所以从 Java 23 开始只要 -XX:+UseZGC.
说到 GC, 这里有个疑问, 为什么 Java 程序会更有意识的去了解不同的 GC 算法, 而其他带 GC 的语言, 如 C#, 及各种脚本型的语言 Python, Node.js, Ruby, 甚至 Go, 就好像 GC 不存在一般, 没人真正去关心, 或者它们的历史和如何持续改进的.
440: Record Patterns - 记录类与模式匹配
Record 自 Java 16 出现以来, 似乎普及率不高, 因为它实现的是一个不可变的数据类, 不及 Lombok 注解生成的数据类灵活. Java 的 enum,
Sealed class, Record 在应用于模式匹配时, 三个都打不过 Rust 的 enum.
记录类支持模式匹配是用
if, switch-case 语句中不仅能进行类型判断, 还能提取其中的值, 如
1record Point(int x, int y) {}
2
3Object obj = new Point(10, 20);
4if (obj instanceof Point(int x, int y)) {
5 System.out.println("Point coordinates: x=" + x + ", y=" + y);
6}
switch 语句中
1switch (obj) {
2 case Point(int x, int y) when x == y -> {
3 System.out.println("Point coordinates: x=" + x + ", y=" + y + ", x equals y");
4 }
5 case Point(int x, int y) -> {
6 System.out.println("Point coordinates: x=" + x + ", y=" + y);
7 }
8 case null, default -> {
9 System.out.println("default point coordinates");
10 }
11}
还支持嵌套拆解
1record Rectangle(Point upperLeft, Point lowerRight) {}
2
3Object obj = new Rectangle(new Point(0, 0), new Point(400, 300));
4
5if (obj instanceof Rectangle(Point(int x1, int y1), var lowerRight)) {
6 System.out.println("Rectangle from (" + x1 + ", " + y1 + ") to (" + lowerRight.x() + ", " + lowerRight.y() + ")");
7}
441: Pattern Matching for switch
Java 这种渐进式加进来的特性很容易被人忽略, 如果正在使用一个像 IntelliJ IDEA 那样智能的 IDE, 很多语言特性都是靠 IDE 推荐的, 不像 JDK 5
的泛型, Java 8 的 Lambda/Stream 那种革命性的变化. 其实 Java 后来新加的一些小特性, 像 record, sealed class, pattern matching,
甚至是 var 类型推断都是悄无声息的进来, 带不起半点涟漪.
Java 21 对 switch/case 模式匹配的增加是把原来要 if (obj instance of Integer i) 的写法加到了 switch/case 语法中. 下面有些地方
直接引用了 JEP 441 中的示例代码
1// Java 21 之前
2static String formatter(Object obj) {
3 String formatted = "unknown";
4 if (obj instanceof Integer i) {
5 formatted = String.format("int %d", i);
6 } else if (obj instanceof Long l) {
7 formatted = String.format("long %d", l);
8 } else if (obj instanceof Double d) {
9 formatted = String.format("double %f", d);
10 } else if (obj instanceof String s) {
11 formatted = String.format("String %s", s);
12 }
13 return formatted;
14}
1// Java 21 开始
2static String formatterPatternSwitch(Object obj) {
3 return switch (obj) {
4 case Integer i -> String.format("int %d", i);
5 case Long l -> String.format("long %d", l);
6 case Double d -> String.format("double %f", d);
7 case String s -> String.format("String %s", s);
8 default -> obj.toString();
9 };
10}
加入了 case null 支持
1// Java 21 开始
2static void testFooBarNew(String s) {
3 switch (s) {
4 case null -> System.out.println("Oops");
5 case "Foo", "Bar" -> System.out.println("Great");
6 default -> System.out.println("Ok");
7 }
8}
case 中加入了 when 条件判断
这样在 case 条件可以更细化了, 其实在介绍 Record 模式匹配时就用到了.
1static void testStringEnhanced(String response) {
2 switch (response) {
3 case null -> { }
4 case "y", "Y" -> {
5 System.out.println("You got it");
6 }
7 case "n", "N" -> {
8 System.out.println("Shame");
9 }
10 case String s when s.equalsIgnoreCase("YES") -> {
11 System.out.println("You got it");
12 }
13 case String s when s.equalsIgnoreCase("NO") -> {
14 System.out.println("Shame");
15 }
16 case String s -> {
17 System.out.println("Sorry?");
18 }
19 }
20}
switch 匹配与 sealed class
1 sealed interface S permits A, B, C {}
2 final class A implements S {}
3 final class B implements S {}
4 record C(int i) implements S {} // Implicitly final
5
6 static int testSealedExhaustive(S s) {
7 return switch (s) {
8 case A a -> 1;
9 case B b -> 2;
10 case C c -> 3;
11 };
12 }
关于模式匹配应该大胆去尝试自己想当然的写法, 试探 Java 的底线.
445: Unnamed Classes and Instance main Method(Preview)
提一下这个特性, 觉得也不是很重要, 这是给 Java 初学者的一个便利. 有不少新语言学习者时常抱怨写一个 Java 的 Hello World 为何要写这么多行代码, 希望象所有的其他脚本语言的 Hello World 基本就是一行, 如 Python 的
1print("Hello World")
而 Java 里要写成
1public class HelloWorld {
2 public static void main(String[] args) {
3 System.out.println("Hello, World!");
4 }
5}
可以写成
1// main.java
2void main() {
3 System.out.println("Hello, World!");
4}
编译之后的类文件名是源文件对应的 main.class, 代码反编译后为
1final class main {
2 void main() {
3 System.out.println("Hello, World!");
4 }
5}
如果源代码文件名为 Main.java, 则生成的类名为 Main.class, 在 Maven 项目中即使文件路径是 src/main/java/org/example/Main.java,
生成的类仍能是 target/classes/Main.class, 并且在 Main.java 不能使用 package 声明.
不过可以声明实例变量, 如下面的 Main.java
1String greeting = "Hello, World!";
2
3void main() {
4 System.out.println(greeting);
5}
产生的类 Main.class 反编译后的代码是
1final class Main {
2 String greeting = "Hello, World!";
3
4 void main() {
5 System.out.println(this.greeting);
6 }
7}
当然也能用 java Main.java 直接运行. 只是这种特性在正式的项目中是不会采用的.
其他值得一提的杂项变化
java.lang.Character 加入了 Emoji 的相关方法
- isEmoji(int codePoint)
- isEmojiPresentation(int codePoint)
- isEmojiModifier(int codePoint)
- isEmojiModifierBase(int codePoint)
- isEmojiComponent(int codePoint)
- isExtendedPictographic(int codePoint)
String 和 java.util.regex.Pattern 中新加了 splitWithDelimiters() 方法
它与 split() 方法不同的是, 不仅返回切分后的字符串结果, 还返回分一处具体匹配到的分隔符, 如
1 String[] strings = "a:b::c:::d".splitWithDelimiters(":+", 5);
2 System.out.println(String.join(", ", strings));
输出为
a, :, b, ::, c, :::, d
StringBuffer 和 StringBuilder 中添加了 repeat(int codePoint, int count) 方法
1new StringBuilder().repeat('-', 80);
[版权声明]
本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。