从 JDK 17 到 21:正式特性、模式匹配与虚拟线程
从 17 升到 21,容易混淆的不是语法,而是“这个版本能用”和“这个版本已正式支持”的区别。运行时升级不会自动改写源码;团队应先确定目标版本,再决定是否接受预览特性。本文限定 Java 21 的规则,不把后续版本的行为倒推回来。
正式与预览:先确定编译契约
正式特性在对应版本中默认可用。预览特性虽然已有完整设计与实现,仍可能调整或移除,不代表未来兼容承诺。根据 Oracle 预览特性说明,编译和运行都需要显式开启,并使用对应版本的 JDK;不能用 JDK 21 加 --enable-preview --release 17 重现 17 的预览语法。
本目录的 PreviewMain.java 使用 Java 21 的未命名类与实例 main 方法,默认编译预期失败;这与普通类中 public static void main(String[] args) 不同。独立试验可使用以下命令,本文业务示例无需这些开关:
javac --enable-preview --release 21 -d out PreviewMain.java
java --enable-preview -cp out PreviewMain预览 class 文件也绑定其发布版本,不能把“换个更新 JVM”视为兼容方案。IDE、命令行和部署启动参数应保持一致。
版本对照:模式匹配不是单个开关
| 特性 | 首次预览与演进 | Java 17 | Java 21 |
|---|---|---|---|
| record 类 | 14、15 预览,16 正式 | 正式 | 正式 |
| sealed 类与接口 | 15、16 预览,17 正式 | 正式 | 正式 |
| instanceof 类型模式 | 14、15 预览,16 正式 | 正式 | 正式 |
| switch 模式匹配 | 17—20 四轮预览,21 正式 | 预览 | 正式 |
| record patterns | 19、20 预览,21 正式 | 不支持 | 正式 |
| virtual threads | 19、20 预览,21 正式 | 不支持 | 正式 |
语言版本依据 Oracle Java 21 语言变更记录;虚拟线程沿革见 Oracle 团队的 Java 21 发布说明。普通 switch 表达式早在 14 已正式化,不能因 Java 17 的模式 switch 尚处预览,就把所有箭头 case 都归为预览语法。
records 与 sealed:数据形状和类型边界
record 声明一组组件,编译器提供访问器、规范构造器、equals、hashCode 与 toString。组件字段为 final,但引用指向的集合仍可能变化,因此它只提供浅不可变;有集合组件时应按需求做防御性复制。record 隐式 final,可以实现接口,不能再继承业务父类。
sealed 限制直接子类型集合。被许可的子类必须继续声明 final、sealed 或 non-sealed,record 的隐式 final 满足要求。这使编译器有机会判断分支是否覆盖所有可能类型,而不是把继承关系留给运行时猜测。
完整源码位于 UpgradeDemo.java,核心模型与分支如下:
sealed interface Result permits Success, Failure {}
record Success(String value) implements Result {}
record Failure(int code) implements Result {}
static String describe(Result result) {
return switch (result) {
case null -> "missing";
case Success(String value) when !value.isBlank() -> "ok:" + value;
case Success(var value) -> "empty";
case Failure(int code) -> "error:" + code;
};
}这里 record 定义数据,sealed 约束变体,record pattern 拆出组件,when 增加守卫条件。非空字符串先匹配带守卫分支;空串落到后面的 Success 分支。宽泛分支放在前面可能遮蔽后续分支并导致编译错误。case null 显式处理空引用;穷尽类型不等于自动处理 null。
NonExhaustive.java 故意遗漏 Failure,属于预期编译失败样例。省略兜底 default 能让新增子类型暴露为编译错误,提醒业务补齐语义。传入 null 的行为与遗漏子类型是两回事。
虚拟线程:改变等待成本,不增加 CPU
Java 21 的虚拟线程适合大量等待 I/O 的任务。它仍需要平台线程执行指令;在可卸载的阻塞点,运行时可释放承载线程执行其他任务。CPU 密集计算并不会因此更快。Java 21 中,在 synchronized 内发生长时间阻塞可能钉住承载线程,应结合实际观测处理,而非机械替换所有锁。依据 Oracle Java 21 虚拟线程指南。
示例先提交两个任务,再按提交顺序调用 Future.get 输出,因此输出顺序稳定,却不能推断任务完成顺序。第一个任务用 sleep 模拟等待,并通过 isVirtual 验证线程身份;没有调用数据库。try-with-resources 在退出作用域时关闭执行器并等待任务结束。
每任务一个虚拟线程不等于无限开放下游容量。数据库连接数、外部接口配额仍需连接池或信号量约束;不要用虚拟线程池复用线程来代替资源限流,也避免给海量线程各缓存大对象。
用 JDK 21 复现,区分源码与运行时目标
在仓库根目录执行:
javac -encoding UTF-8 --release 21 -d docs/upgrade-100/checks/jdk17-to21/classes examples/java-advanced/jdk17-to21/UpgradeDemo.java
java -cp docs/upgrade-100/checks/jdk17-to21/classes UpgradeDemo本地 JDK 21.0.12 实际输出:
instanceof=java21
virtual=true, ok:java21
error:503
empty
missing命令、版本、退出码及失败诊断保存在 运行证据。JDK 21 使用 --release 17 编译该示例预期失败:该参数约束语言及平台 API,不能把 21 的特性降级翻译给 17。此次未使用 JDK 17 运行时,也未执行数据库、Redis、吞吐量或真实网络测试;sleep 只证明本地任务流程。
继续阅读已有的 接口方法与 JDK 21、类加载器从 JDK 8 到 21 和 ThreadLocal,可分别补充 API 演进、运行时边界与线程上下文的理解。
