Skip to content

JDK 17 新增特性、使用方法与迁移实战

JDK 17是现代企业Java的重要LTS,也是Spring Boot 3的最低Java基线。若从JDK 11升级到17,会累计获得JDK 12~17的正式语言/API能力,同时面对更严格的JDK内部封装、旧组件移除和运行时默认变化。

一、JDK 12到17能力地图

首次正式版本能力用途
JDK 14Switch表达式返回值、避免break贯穿
JDK 14Helpful NullPointerException更精确定位NPE表达式
JDK 15文本块SQL、JSON、多行文本
JDK 15Hidden Classes框架动态生成不可发现类
JDK 16Record简洁表达不可变数据载体
JDK 16instanceof模式匹配类型检查和变量绑定合并
JDK 16Stream.toList()简洁收集到不可修改List
JDK 16Unix-Domain Socket本机进程间Socket通信
JDK 17密封类限制类型层次的实现集合
JDK 17增强伪随机数生成器API统一随机算法接口
JDK 17HexFormat十六进制编解码
JDK 17强封装JDK内部API推动框架移除非法深反射

有些能力在更早版本以Preview出现,但表中写的是正式版本。生产代码默认不应依赖Preview特性。

二、Record

java
public record OrderSummary(long orderId, String status, long amountCent) {
    public OrderSummary {
        if (orderId <= 0) {
            throw new IllegalArgumentException("orderId必须大于0");
        }
        if (status == null || status.isBlank()) {
            throw new IllegalArgumentException("status不能为空");
        }
        if (amountCent < 0) {
            throw new IllegalArgumentException("金额不能为负数");
        }
    }
}

编译器为Record生成:

  • private final组件字段。
  • 规范构造器。
  • 与组件同名的访问器,如orderId()
  • 基于所有组件的equalshashCodetoString

适合和不适合

适合不适合或需谨慎
API DTOJPA Entity
查询投影需要可变生命周期的对象
领域值对象需要继承具体父类的模型
事件消息需要隐藏部分组件不参与相等性的类型
Map Key组件本身可变导致的浅不可变问题

Record是浅不可变:组件引用不能重新赋值,但组件指向的List仍可能可变。需要真正不可变时做防御性复制:

java
public record OrderBatch(List<Long> orderIds) {
    public OrderBatch {
        orderIds = List.copyOf(orderIds);
    }
}

Record不是Lombok @Data的简单替代。它明确声明“对象的状态由全部组件定义”,访问器命名、继承限制和序列化行为也不同。

三、密封类

java
public sealed interface PaymentResult
        permits PaySuccess, PayRejected, PayPending {
}

public record PaySuccess(String tradeNo) implements PaymentResult {}
public record PayRejected(String reason) implements PaymentResult {}
public final class PayPending implements PaymentResult {
    private final String queryToken;

    public PayPending(String queryToken) {
        this.queryToken = queryToken;
    }
}

被允许的直接子类型必须明确选择:

修饰符含义
final到此结束,不允许继续继承
sealed继续限制下一层允许的子类型
non-sealed从这一层重新开放继承

适合:支付结果、审批状态、协议消息、表达式树等有限类型集合。不适合第三方自由扩展的SPI,否则会破坏插件能力。

在JDK 17中,Switch类型模式匹配仍是Preview,不能把“sealed+穷尽Switch模式”当成无需Preview即可使用的JDK 17正式组合。可以使用instanceof链,或升级到JDK 21使用正式Switch模式匹配。

四、Switch表达式

java
static int priority(String level) {
    return switch (level) {
        case "URGENT" -> 100;
        case "HIGH" -> 80;
        case "NORMAL" -> 50;
        default -> throw new IllegalArgumentException("未知级别:" + level);
    };
}

复杂分支使用yield

java
int fee = switch (channel) {
    case "VIP" -> 0;
    case "NORMAL" -> 500;
    default -> {
        auditUnknown(channel);
        yield 1000;
    }
};

优势:

  • 可以直接产生值。
  • 箭头分支不会意外贯穿。
  • 编译器检查结果必须被赋值或抛异常。
  • Enum场景更容易表达穷尽分支。

传统冒号Switch语法仍可使用;迁移时不要混合复杂贯穿逻辑和表达式返回,优先保持分支清晰。

五、文本块

java
String query = """
        SELECT id, status, amount_cent
        FROM t_order
        WHERE tenant_id = ?
          AND created_at >= ?
        ORDER BY id DESC
        """;

JSON示例:

java
String json = """
        {
          "orderId": 1001,
          "status": "PAID"
        }
        """;

文本块仍是String,不是模板语言。注意:

  • 公共缩进会被处理。
  • 起止位置会影响首尾换行。
  • SQL参数仍应使用PreparedStatement,文本块不防注入。
  • JSON仍应由序列化库生成,手写文本块适合测试样例而不是任意动态数据。
  • 签名、协议或快照测试应断言最终字符和字节。

六、instanceof模式匹配

java
static String normalize(Object value) {
    if (value instanceof String text && !text.isBlank()) {
        return text.strip();
    }
    return "UNKNOWN";
}

变量text只在编译器能证明类型判断成立的控制流范围内可见:

java
if (!(value instanceof String text)) {
    return "UNKNOWN";
}
return text.strip();

它减少重复强转,但不会改变null instanceof Type为false的规则,也不等于JDK 21的Record模式解构。

七、Stream.toList

java
List<Long> paidIds = orders.stream()
        .filter(Order::isPaid)
        .map(Order::getId)
        .toList();

Stream.toList()返回不可修改List,调用add通常抛UnsupportedOperationException。不要把它与Collectors.toList()当成完全相同契约:后者没有承诺具体类型或可变性,但常见实现可能可变。需要明确可变结果时:

java
List<Long> mutable = orders.stream()
        .map(Order::getId)
        .collect(Collectors.toCollection(ArrayList::new));

八、Helpful NullPointerException

java
String city = order.getUser().getAddress().getCity();

现代JDK可以在NPE信息中更精确指出链条中哪个表达式为null,降低排查成本。但它不是空值设计方案,不能代替参数校验、领域约束和合理的Optional边界。异常信息也可能包含类和字段结构,不应原样暴露给外部用户。

九、HexFormat

java
import java.util.HexFormat;

HexFormat format = HexFormat.of();
String hex = format.formatHex(new byte[] {0x01, 0x2A, (byte) 0xFF});
byte[] bytes = format.parseHex(hex);

可配置分隔符和大小写:

java
String text = HexFormat.ofDelimiter(":")
        .withUpperCase()
        .formatHex(bytes);

十六进制和Base64都是编码,不是加密或密码哈希。

十、增强伪随机数生成器API

JDK 17提供RandomGenerator统一接口:

java
import java.util.random.RandomGenerator;

RandomGenerator generator = RandomGenerator.of("L64X128MixRandom");
int shard = generator.nextInt(16);

可以枚举算法:

java
RandomGeneratorFactory.all()
        .map(RandomGeneratorFactory::name)
        .sorted()
        .forEach(System.out::println);

普通随机模拟、采样和测试可使用RandomGenerator;密码、Token、验证码等安全敏感场景继续使用SecureRandom,不能因为算法名字复杂就假设具备密码学安全性。

十一、Unix-Domain Socket

JDK 16为SocketChannel等提供Unix-domain socket支持,适合同一主机进程间通信:

java
UnixDomainSocketAddress address =
        UnixDomainSocketAddress.of("/tmp/order.sock");

try (SocketChannel channel = SocketChannel.open(StandardProtocolFamily.UNIX)) {
    channel.connect(address);
    channel.write(StandardCharsets.UTF_8.encode("ping"));
}

依赖操作系统支持,需要处理路径权限、残留socket文件、容器挂载和平台差异。跨主机通信仍应使用INET协议。

十二、强封装JDK内部API

Java 9引入模块边界,JDK 17进一步严格封装JDK内部元素。旧框架可能出现:

text
java.lang.reflect.InaccessibleObjectException

错误路径:

text
旧框架深反射JDK内部字段
    → JDK 17模块未opens
    → setAccessible失败

处理优先级:

  1. jdeps --jdk-internals定位内部API。
  2. 升级框架、字节码库、序列化库和Agent。
  3. 改用公开API或正确模块声明。
  4. 只有无法立即升级时才临时使用最小范围--add-opens
  5. 给临时参数设置负责人和下线计划。
bash
--add-opens java.base/java.lang=ALL-UNNAMED

这不是通用推荐参数,不应复制到所有应用。

十三、运行时和JVM变化

统一日志

bash
-Xlog:gc*,safepoint:file=/logs/gc.log:time,uptime,level,tags:filecount=10,filesize=100M

Java 8的PrintGCDetailsPrintGCDateStampsXloggc等参数不能原样照搬。

容器感知

现代JDK更好识别cgroup的CPU和内存限制:

bash
java -XX:InitialRAMPercentage=30 \
     -XX:MaxRAMPercentage=65 \
     -jar app.jar

容器内存还包括Metaspace、线程栈、直接内存、Code Cache、GC结构和本地库。Xmx接近容器limit可能触发OOMKill,即使Heap没有Java OOM。

GC演进

变化版本边界
G1成为常见服务器默认JDK 9起
Epsilon实验性引入JDK 11
ZGC实验性引入JDK 11,后续逐步成熟
ZGC成为产品特性JDK 15
Shenandoah成为产品特性JDK 15常见OpenJDK主线能力
CMS移除JDK 14

具体发行版可能存在差异,生产要通过运行时Flags和供应商文档确认。

十四、移除和废弃能力

能力变化迁移影响
CMSJDK 14移除删除CMS参数,重新选GC和压测
Pack200JDK 14移除旧打包和分发工具需替换
NashornJDK 15移除替换脚本引擎或消除依赖
RMI ActivationJDK 17移除历史分布式激活方案需迁移
Security ManagerJDK 17标记弃用并计划移除不再作为新应用沙箱方案
Applet APIJDK 17标记计划移除遗留桌面/浏览器方案迁移

十五、JDK 11升级17 Runbook

mermaid
flowchart TD
    A["确认框架和依赖支持17"] --> B["升级构建插件与Agent"]
    B --> C["jdeps扫描内部API"]
    C --> D["处理强封装和移除组件"]
    D --> E["转换失效JVM参数"]
    E --> F["功能、序列化、TLS和性能测试"]
    F --> G["灰度与回滚"]

重点检查:

  • Spring Boot 2.x/3.x准确版本,不要反推兼容性。
  • Byte Buddy、ASM、CGLIB、Mockito、Lombok、MapStruct。
  • JaCoCo、Surefire、编译插件和Gradle版本。
  • APM、安全、Profiler Agent。
  • Jackson、Hibernate、数据库驱动和序列化兼容。
  • --illegal-access依赖和--add-opens
  • CMS、旧GC日志和已移除JVM参数。
  • Docker基础镜像、CA证书、字体、时区和架构。
  • 性能基线、GC、RSS、P95/P99和CPU throttling。

十六、常见误区

误区正确理解
Record就是不可变EntityRecord浅不可变且不适合多数代理Entity
Record等于Lombok Data语义、继承和访问器都不同
JDK 17已正式支持Switch类型模式JDK 17仍是Preview,JDK 21正式
Stream.toList等同于Collectors.toList可修改契约不同
强封装报错永久加add-opens即可应优先升级依赖和公开API
新JDK默认GC一定更快必须用真实负载比较吞吐和延迟
容器只需设置Xmx还要预算非堆和本地内存

十七、面试回答

JDK 17新增了什么

从语言和API看,JDK 12~17累计正式提供Switch表达式、文本块、Record、instanceof模式匹配、密封类、Stream.toList、HexFormat和增强随机数API;运行时方面更严格封装JDK内部API,统一日志、容器感知和现代GC能力更加成熟,同时CMS、Nashorn等历史组件已移除。Spring Boot 3最低需要Java 17,因此升级还常伴随Spring 6和Jakarta生态迁移,但JDK升级与框架升级应分层分析。

Record适合什么场景

适合DTO、查询投影、事件消息、值对象和Map Key,因为它明确表达由全部组件定义的值语义并生成访问器、equals/hashCode/toString;不适合多数JPA Entity和需要可变生命周期、代理或开放继承的对象。组件仍可能指向可变对象,需要防御性复制。

JDK 11升级17最常见问题

旧框架和Agent深反射JDK内部成员会触发InaccessibleObjectException;CMS和旧JVM参数失效;Nashorn、Pack200等组件移除;旧字节码库和Annotation Processor不认识Class 61。应先升级依赖和构建链,使用jdeps定位内部API,再做功能与性能灰度。

十八、关联学习

专题内容
LTS版本演进总览JDK 8、11、17、21横向比较
JDK 11新增特性11到17升级起点
JDK 21新增特性虚拟线程和正式模式匹配
Java 17现代基线从JDK 8升级17的完整Runbook
反射全过程模块强封装和反射
JVM性能调优GC、容器内存和诊断

本章小结

JDK 17把现代Java的数据建模、分支表达和封装边界推向成熟:Record表达值,sealed控制类型集合,文本块和Switch表达式减少样板,强封装迫使生态摆脱内部API。真正升级成功不仅是代码能编译,还要证明依赖、Agent、GC、容器内存、序列化和性能都满足生产基线。