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()。
  • 基于所有组件的equals、hashCode、toString。

适合和不适合 ​

适合不适合或需谨慎
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的PrintGCDetails、PrintGCDateStamps、Xloggc等参数不能原样照搬。

容器感知 ​

现代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、容器内存、序列化和性能都满足生产基线。