主题
Java 新特性 8–21
一句话:Java 8 到 21 的主线不是「语法变多」,而是把类型安全、不可变数据和并发模型逐步做进语言;真正能进生产的特性集中在 LTS 版本,预览特性不能当成稳定 API。
适用 JDK 8–21,重点标注 JDK 17 / JDK 21。预览特性以 --enable-preview 为准,不能用于生产。
为什么需要
Java 的历史包袱是冗长和保守:数据类要写 getter/setter/equals/hashCode,类型分支要强转,继承层次无法封闭,IO 密集并发只能靠线程池。每代 JDK 都在解决其中一个具体痛点,而不是为了新而新。
判断一个特性是否值得关注,看三点:是否减少一类重复代码,是否把运行期错误提前到编译期,是否改变并发/内存模型。只增加语法糖的特性,优先级可以放低。
核心机制
按版本看真正影响生产的特性
| 版本 | 特性 | 生产影响 |
|---|---|---|
| 8 | Lambda、Stream、Optional、默认方法、CompletableFuture、java.time | 函数式集合处理、异步编排、时间 API 的基础;Optional 是返回类型,不是字段万能药 |
| 9 | 模块系统 JPMS、List.of 等工厂、G1 成为默认 GC、接口私有方法、HTTP/2 Client(孵化) | 模块化对库作者影响大;G1 默认改变 GC 行为;集合工厂返回不可变集合 |
| 10 | var 局部变量类型推断、容器感知 | 只减少局部变量样板;不是动态类型,不能用于字段/方法参数 |
| 11 | HTTP Client 转正、ZGC 实验、Epsilon GC、String 新方法、单文件源码启动 | 标准 HTTP/2 客户端;ZGC 适合低延迟试验,JDK 15 才转正 |
| 12 | switch 表达式(预览)、Shenandoah(实验)、G1 归还空闲内存 | switch 可返回值,减少 fall-through;Shenandoah 依赖发行版 |
| 13 | 文本块(预览)、switch 表达式第二版、动态 CDS | 多行 SQL/JSON 可读性提升,JDK 15 转正 |
| 14 | records(预览)、instanceof 模式匹配(预览)、switch 表达式转正、友好 NPE、CMS 移除 | switch 表达式可用于生产;CMS 退出历史舞台 |
| 15 | 文本块转正、sealed(预览)、隐藏类、ZGC 转正、Nashorn 移除 | 文本块和 ZGC 可用;sealed 为代数数据类型铺路 |
| 16 | records 转正、instanceof 模式匹配转正、sealed 第二版、Stream.toList()、强封装默认开启 | record 可进生产;JDK 内部 API 反射开始被强封装 |
| 17 | sealed 转正、switch 模式匹配(预览)、恢复严格浮点、Security Manager 弃用、FFM API(孵化) | LTS;sealed 可表达封闭继承;Security Manager 进入退场流程 |
| 18 | UTF-8 成为默认字符集、简易 Web 服务器、finalization 弃用、Javadoc 代码片段 | 文件/网络读写不再依赖平台默认编码;旧代码要显式指定字符集 |
| 19 | 虚拟线程(预览)、结构化并发(孵化)、record 模式(预览)、switch 模式第三版 | 虚拟线程开始可用但仍是预览;不要在生产依赖预览 API |
| 20 | 虚拟线程第二版、作用域值(孵化)、record 模式第二版、switch 模式第四版 | 继续打磨;API 仍可能变 |
| 21 | 虚拟线程转正、record 模式转正、switch 模式匹配转正、分代 ZGC、顺序集合、FFM API 转正、字符串模板(预览) | LTS;虚拟线程和模式匹配可进生产;字符串模板仍是预览 |
几个容易记错的归属:
- records 在 14 预览、16 转正,不是 8。
- sealed 在 15 预览、17 转正,不是 11。
- 虚拟线程在 19/20 预览、21 转正,不是 17。
- switch 模式匹配在 17/18/19/20 预览、21 转正。
- 文本块在 13/14 预览、15 转正。
records:数据载体,不是万能 Bean
record 自动生成规范构造器、访问器、equals、hashCode、toString,字段隐式 private final。它适合 DTO、事件、查询结果、模式匹配的载体。
它不适合 JPA 实体:JPA 需要无参构造和可变属性,record 没有 setter,字段不可变。也不适合需要继承的模型,record 不能继承其他类。需要校验时在规范构造器里写,例如 if (r < 0) throw new IllegalArgumentException()。
sealed:把继承关进笼子
sealed interface Shape permits Circle, Square 限定只有列出的子类型可以实现。子类型必须是 final、sealed 或 non-sealed。它让编译器知道「所有可能的分支」,switch 才能做穷尽性检查。
代价是扩展性:外部模块不能再新增实现。它适合领域模型、协议消息、表达式树;不适合作为开放给第三方扩展的 SPI 接口。
模式匹配:类型检查、转换、解构合并
instanceof 模式在 16 转正:
java
if (obj instanceof String s && !s.isBlank()) {
System.out.println(s.length());
}switch 模式在 21 转正,配合 record 模式可以解构:
java
return switch (shape) {
case Circle(double r) -> Math.PI * r * r;
case Square(double s) -> s * s;
};switch 要求穷尽所有分支;没有覆盖 null 时,传入 null 会抛 NPE,需要显式 case null。预览期的语法可能变化,生产代码要确认所用 JDK 的正式状态。
虚拟线程:改变的是等待成本
平台线程绑定操作系统线程,阻塞一次就占一个 OS 线程。虚拟线程由 JVM 调度,阻塞时把载体线程让出来,所以可以用「一个请求一个虚拟线程」的阻塞写法处理大量 IO。
它不改变 CPU 核数,也不让 CPU 密集任务变快。JDK 21 中 synchronized 阻塞可能 pin 住载体线程,ThreadLocal 的复制成本也仍在;JDK 24+ 的 JEP 491 才处理 synchronized 的 pinning。虚拟线程不要池化,用 Executors.newVirtualThreadPerTaskExecutor() 或 Thread.ofVirtual()。
最小上手
用 JDK 21 编译运行 record、sealed、switch 模式和虚拟线程:
java
import java.util.concurrent.*;
sealed interface Shape permits Circle, Square {}
record Circle(double r) implements Shape {}
record Square(double s) implements Shape {}
public class Modern {
static double area(Shape shape) {
return switch (shape) {
case Circle(double r) -> Math.PI * r * r;
case Square(double s) -> s * s;
};
}
public static void main(String[] args) throws Exception {
System.out.println(area(new Circle(1)));
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
var future = executor.submit(() ->
"virtual=" + Thread.currentThread().isVirtual());
System.out.println(future.get());
}
}
}bash
javac --release 21 Modern.java && java Modern
# 3.141592653589793
# virtual=true把 --release 21 改成 17,record 模式会编译失败;这能直观看到版本归属,而不是靠记忆。
生产动作
- 选 LTS:新项目优先 JDK 17 或 21;非 LTS 只用于验证特性,不承诺长期补丁。
- 编译目标固定:Maven 用
<release>21</release>,不要只设source/target;后者不会校验 API 的可用版本。 - 预览特性隔离:
--enable-preview的类文件必须用同一 JDK 运行,不能进生产;框架和中间件通常不支持预览 API。 - UTF-8 迁移:JDK 18 起默认字符集是 UTF-8(JEP 400)。读写文件、启动子进程、解析 HTTP 时显式写
StandardCharsets.UTF_8,避免依赖平台默认值。 - 强封装:JDK 16 起默认强封装,JDK 17 移除
--illegal-access的宽松模式;反射 JDK 内部 API 要用--add-opens,并记录原因。 - 虚拟线程压测:用真实下游延迟压测,观察载体线程数、pin 事件和内存;不要用 CPU 密集任务验证虚拟线程收益。
边界
- record 不是可变 Bean:需要 setter、无参构造、代理增强的框架(JPA、部分序列化库)要先验证兼容性。
- sealed 限制扩展:对外开放的插件接口不要 sealed;它适合封闭领域模型。
- 模式匹配不是运行时反射:它依赖编译期类型信息,泛型擦除后不能匹配
List<String>。 - 虚拟线程不适合 CPU 密集:它解决的是阻塞等待的成本,不是计算吞吐。
- 预览特性不是「提前用」:API 可能改名或语义变化,升级 JDK 时编译失败是常态。
讲清楚
Java 8 到 21 的主线是把重复代码和运行期错误逐步收进语言:Lambda/Stream 减少集合样板,record 减少数据类样板,sealed 让继承封闭,模式匹配把类型检查和转换合并,虚拟线程降低阻塞等待的线程成本。
版本归属要记准:record 16 转正、sealed 17 转正、switch 模式 21 转正、虚拟线程 21 转正。生产只选 LTS,预览特性不进生产;record 不适合 JPA 实体,虚拟线程不适合 CPU 密集任务,sealed 不适合开放插件接口。升级前先看字符集、强封装和 GC 默认值的变化。