跳到正文

Java 新特性 8–21 ​

一句话:Java 8 到 21 的主线不是「语法变多」,而是把类型安全、不可变数据和并发模型逐步做进语言;真正能进生产的特性集中在 LTS 版本,预览特性不能当成稳定 API。

适用 JDK 8–21,重点标注 JDK 17 / JDK 21。预览特性以 --enable-preview 为准,不能用于生产。

为什么需要 ​

Java 的历史包袱是冗长和保守:数据类要写 getter/setter/equals/hashCode,类型分支要强转,继承层次无法封闭,IO 密集并发只能靠线程池。每代 JDK 都在解决其中一个具体痛点,而不是为了新而新。

判断一个特性是否值得关注,看三点:是否减少一类重复代码,是否把运行期错误提前到编译期,是否改变并发/内存模型。只增加语法糖的特性,优先级可以放低。

核心机制 ​

按版本看真正影响生产的特性 ​

版本特性生产影响
8Lambda、Stream、Optional、默认方法、CompletableFuture、java.time函数式集合处理、异步编排、时间 API 的基础;Optional 是返回类型,不是字段万能药
9模块系统 JPMS、List.of 等工厂、G1 成为默认 GC、接口私有方法、HTTP/2 Client(孵化)模块化对库作者影响大;G1 默认改变 GC 行为;集合工厂返回不可变集合
10var 局部变量类型推断、容器感知只减少局部变量样板;不是动态类型,不能用于字段/方法参数
11HTTP Client 转正、ZGC 实验、Epsilon GC、String 新方法、单文件源码启动标准 HTTP/2 客户端;ZGC 适合低延迟试验,JDK 15 才转正
12switch 表达式(预览)、Shenandoah(实验)、G1 归还空闲内存switch 可返回值,减少 fall-through;Shenandoah 依赖发行版
13文本块(预览)、switch 表达式第二版、动态 CDS多行 SQL/JSON 可读性提升,JDK 15 转正
14records(预览)、instanceof 模式匹配(预览)、switch 表达式转正、友好 NPE、CMS 移除switch 表达式可用于生产;CMS 退出历史舞台
15文本块转正、sealed(预览)、隐藏类、ZGC 转正、Nashorn 移除文本块和 ZGC 可用;sealed 为代数数据类型铺路
16records 转正、instanceof 模式匹配转正、sealed 第二版、Stream.toList()、强封装默认开启record 可进生产;JDK 内部 API 反射开始被强封装
17sealed 转正、switch 模式匹配(预览)、恢复严格浮点、Security Manager 弃用、FFM API(孵化)LTS;sealed 可表达封闭继承;Security Manager 进入退场流程
18UTF-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 默认值的变化。

把一门技术讲透到既懂也会用