学完这篇你能得到什么:用最短的时间把 Java 21 三个最实用的新特性(虚拟线程、switch 模式匹配、记录类与记录模式)跑通,并知道它们各自能替换掉你项目里的哪段旧代码。
Java 21 是继 17 之后的又一个 LTS 版本,2023 年 9 月发布。下面的示例默认你已装好 JDK 21,用 java -version 能看到 21.x。本文涉及的特性全部是正式转正的,不需要加 --enable-preview。
第一步:把虚拟线程跑起来
虚拟线程(JEP 444)解决的是一件事:让"一个请求一个线程"这种最好写的模型,在几千上万并发下也能撑住。它由 JVM 调度,不是操作系统线程,创建和切换的代价极小。
最直接的两种写法:
// 写法 A:单个启动
Thread t = Thread.ofVirtual().name("task-", 0).start(() -> {
System.out.println("运行在:" + Thread.currentThread());
});
t.join();
// 写法 B:批量执行(推荐)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
executor.submit(() -> {
Thread.sleep(1000); // 模拟阻塞 IO
return 1;
});
}
} // try-with-resources 会自动 close 并等待全部完成
写法 B 里每个任务一个虚拟线程,没有池化。想对比平台线程,把 newVirtualThreadPerTaskExecutor() 换成 newFixedThreadPool(200),同样的代码会慢一大截。
注意:虚拟线程不要池化。它便宜到可以随用随建,池化反而会带来排队和状态残留问题。同时它只适合 IO 密集(数据库、HTTP、文件)场景,纯 CPU 计算用它没有任何收益,Thread.sleep 之外的忙等同样会占住载体线程。
注意:在 Java 21 上,synchronized 块里的阻塞操作会"钉住"(pin)载体线程,导致并行度掉下来。热点代码里把 synchronized 换成 ReentrantLock 即可绕开(这个问题在后续版本已被官方修复,但 21 上仍需注意)。
第二步:用 switch 做类型分发
以前写类型判断要么一串 if (x instanceof A),要么 switch 只能比字符串和数字。JEP 441 让 switch 可以直接按类型匹配,还能加守卫条件:
static String describe(Object o) {
return switch (o) {
case null -> "空值";
case Integer i when i > 0 -> "正整数 " + i;
case Integer i -> "非正整数 " + i;
case String s -> "字符串,长度 " + s.length();
case int[] arr -> "数组,长度 " + arr.length;
default -> "其他类型:" + o.getClass().getSimpleName();
};
}
两个容易忽略的点:case null 需要显式写出来(否则抛 NPE);如果不是密封类体系,default 分支不能省。
第三步:记录类 + 记录模式
记录类(record)在 Java 16 就转正了,Java 21 补上的是记录模式(JEP 440)——可以直接把 record 拆开取值,而且能嵌套。
record Point(int x, int y) {}
record Line(Point start, Point end) {}
static boolean isDiagonal(Object o) {
return o instanceof Line(Point(int x1, int y1), Point(int x2, int y2))
&& Math.abs(x2 - x1) == Math.abs(y2 - y1);
}
这段代码在旧写法里要写四行 instanceof + 四次强转。更实用的是和 switch 组合:
sealed interface Shape permits Circle, Rect {}
record Circle(Point center, int r) implements Shape {}
record Rect(Point lt, Point rb) implements Shape {}
static double area(Shape s) {
return switch (s) {
case Circle(Point c, int r) -> Math.PI * r * r;
case Rect(Point(int x1, int y1), Point(int x2, int y2))
-> Math.abs((x2 - x1) * (y2 - y1));
};
}
因为 Shape 是 sealed,编译器知道只有两种实现,default 可以省掉——以后新增实现类时,这里会编译期报错提醒你补分支,这正是密封类 + 模式匹配组合的价值。
注意:record 的字段是 final 的,它天生适合当 DTO、返回值、消息载体,不适合做需要可变状态的实体。别为了用而用,把它套在有 setter 需求的类上会很难受。
第四步:顺手用上的两个小特性
Sequenced Collections(JEP 431):List、Deque、LinkedHashSet、LinkedHashMap 现在都有统一的顺序访问 API,不用再记 get(0) / get(size()-1) / firstKey() 这些各写各的写法:
list.getFirst();
list.getLast();
map.sequencedKeySet().reversed();
字符串模板在 21 里是预览特性,需要 --enable-preview,尝鲜可以但不建议上生产。
小结
- 虚拟线程用
Executors.newVirtualThreadPerTaskExecutor(),别池化、只用于 IO 密集场景,热点里避开 synchronized。
- switch 模式匹配支持类型、守卫
when、null 分支;非密封体系记得写 default。
- 记录模式能嵌套解构,配合
sealed 接口可以让编译器帮你检查分支遗漏。
Sequenced Collections 统一了首尾访问写法,属于零成本升级,可以直接改。
- 三者是正交的:
record 定义数据、模式匹配解构数据、虚拟线程承载数据的处理,组合起来才是 Java 21 最舒服的写法。