JDK 17 新增特性、使用方法与迁移实战
JDK 17是现代企业Java的重要LTS,也是Spring Boot 3的最低Java基线。若从JDK 11升级到17,会累计获得JDK 12~17的正式语言/API能力,同时面对更严格的JDK内部封装、旧组件移除和运行时默认变化。
一、JDK 12到17能力地图
| 首次正式版本 | 能力 | 用途 |
|---|---|---|
| JDK 14 | Switch表达式 | 返回值、避免break贯穿 |
| JDK 14 | Helpful NullPointerException | 更精确定位NPE表达式 |
| JDK 15 | 文本块 | SQL、JSON、多行文本 |
| JDK 15 | Hidden Classes | 框架动态生成不可发现类 |
| JDK 16 | Record | 简洁表达不可变数据载体 |
| JDK 16 | instanceof模式匹配 | 类型检查和变量绑定合并 |
| JDK 16 | Stream.toList() | 简洁收集到不可修改List |
| JDK 16 | Unix-Domain Socket | 本机进程间Socket通信 |
| JDK 17 | 密封类 | 限制类型层次的实现集合 |
| JDK 17 | 增强伪随机数生成器API | 统一随机算法接口 |
| JDK 17 | HexFormat | 十六进制编解码 |
| JDK 17 | 强封装JDK内部API | 推动框架移除非法深反射 |
有些能力在更早版本以Preview出现,但表中写的是正式版本。生产代码默认不应依赖Preview特性。
二、Record
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 DTO | JPA Entity |
| 查询投影 | 需要可变生命周期的对象 |
| 领域值对象 | 需要继承具体父类的模型 |
| 事件消息 | 需要隐藏部分组件不参与相等性的类型 |
| Map Key | 组件本身可变导致的浅不可变问题 |
Record是浅不可变:组件引用不能重新赋值,但组件指向的List仍可能可变。需要真正不可变时做防御性复制:
public record OrderBatch(List<Long> orderIds) {
public OrderBatch {
orderIds = List.copyOf(orderIds);
}
}Record不是Lombok @Data的简单替代。它明确声明“对象的状态由全部组件定义”,访问器命名、继承限制和序列化行为也不同。
三、密封类
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表达式
static int priority(String level) {
return switch (level) {
case "URGENT" -> 100;
case "HIGH" -> 80;
case "NORMAL" -> 50;
default -> throw new IllegalArgumentException("未知级别:" + level);
};
}复杂分支使用yield:
int fee = switch (channel) {
case "VIP" -> 0;
case "NORMAL" -> 500;
default -> {
auditUnknown(channel);
yield 1000;
}
};优势:
- 可以直接产生值。
- 箭头分支不会意外贯穿。
- 编译器检查结果必须被赋值或抛异常。
- Enum场景更容易表达穷尽分支。
传统冒号Switch语法仍可使用;迁移时不要混合复杂贯穿逻辑和表达式返回,优先保持分支清晰。
五、文本块
String query = """
SELECT id, status, amount_cent
FROM t_order
WHERE tenant_id = ?
AND created_at >= ?
ORDER BY id DESC
""";JSON示例:
String json = """
{
"orderId": 1001,
"status": "PAID"
}
""";文本块仍是String,不是模板语言。注意:
- 公共缩进会被处理。
- 起止位置会影响首尾换行。
- SQL参数仍应使用PreparedStatement,文本块不防注入。
- JSON仍应由序列化库生成,手写文本块适合测试样例而不是任意动态数据。
- 签名、协议或快照测试应断言最终字符和字节。
六、instanceof模式匹配
static String normalize(Object value) {
if (value instanceof String text && !text.isBlank()) {
return text.strip();
}
return "UNKNOWN";
}变量text只在编译器能证明类型判断成立的控制流范围内可见:
if (!(value instanceof String text)) {
return "UNKNOWN";
}
return text.strip();它减少重复强转,但不会改变null instanceof Type为false的规则,也不等于JDK 21的Record模式解构。
七、Stream.toList
List<Long> paidIds = orders.stream()
.filter(Order::isPaid)
.map(Order::getId)
.toList();Stream.toList()返回不可修改List,调用add通常抛UnsupportedOperationException。不要把它与Collectors.toList()当成完全相同契约:后者没有承诺具体类型或可变性,但常见实现可能可变。需要明确可变结果时:
List<Long> mutable = orders.stream()
.map(Order::getId)
.collect(Collectors.toCollection(ArrayList::new));八、Helpful NullPointerException
String city = order.getUser().getAddress().getCity();现代JDK可以在NPE信息中更精确指出链条中哪个表达式为null,降低排查成本。但它不是空值设计方案,不能代替参数校验、领域约束和合理的Optional边界。异常信息也可能包含类和字段结构,不应原样暴露给外部用户。
九、HexFormat
import java.util.HexFormat;
HexFormat format = HexFormat.of();
String hex = format.formatHex(new byte[] {0x01, 0x2A, (byte) 0xFF});
byte[] bytes = format.parseHex(hex);可配置分隔符和大小写:
String text = HexFormat.ofDelimiter(":")
.withUpperCase()
.formatHex(bytes);十六进制和Base64都是编码,不是加密或密码哈希。
十、增强伪随机数生成器API
JDK 17提供RandomGenerator统一接口:
import java.util.random.RandomGenerator;
RandomGenerator generator = RandomGenerator.of("L64X128MixRandom");
int shard = generator.nextInt(16);可以枚举算法:
RandomGeneratorFactory.all()
.map(RandomGeneratorFactory::name)
.sorted()
.forEach(System.out::println);普通随机模拟、采样和测试可使用RandomGenerator;密码、Token、验证码等安全敏感场景继续使用SecureRandom,不能因为算法名字复杂就假设具备密码学安全性。
十一、Unix-Domain Socket
JDK 16为SocketChannel等提供Unix-domain socket支持,适合同一主机进程间通信:
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内部元素。旧框架可能出现:
java.lang.reflect.InaccessibleObjectException错误路径:
旧框架深反射JDK内部字段
→ JDK 17模块未opens
→ setAccessible失败处理优先级:
- 用
jdeps --jdk-internals定位内部API。 - 升级框架、字节码库、序列化库和Agent。
- 改用公开API或正确模块声明。
- 只有无法立即升级时才临时使用最小范围
--add-opens。 - 给临时参数设置负责人和下线计划。
--add-opens java.base/java.lang=ALL-UNNAMED这不是通用推荐参数,不应复制到所有应用。
十三、运行时和JVM变化
统一日志
-Xlog:gc*,safepoint:file=/logs/gc.log:time,uptime,level,tags:filecount=10,filesize=100MJava 8的PrintGCDetails、PrintGCDateStamps、Xloggc等参数不能原样照搬。
容器感知
现代JDK更好识别cgroup的CPU和内存限制:
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和供应商文档确认。
十四、移除和废弃能力
| 能力 | 变化 | 迁移影响 |
|---|---|---|
| CMS | JDK 14移除 | 删除CMS参数,重新选GC和压测 |
| Pack200 | JDK 14移除 | 旧打包和分发工具需替换 |
| Nashorn | JDK 15移除 | 替换脚本引擎或消除依赖 |
| RMI Activation | JDK 17移除 | 历史分布式激活方案需迁移 |
| Security Manager | JDK 17标记弃用并计划移除 | 不再作为新应用沙箱方案 |
| Applet API | JDK 17标记计划移除 | 遗留桌面/浏览器方案迁移 |
十五、JDK 11升级17 Runbook
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就是不可变Entity | Record浅不可变且不适合多数代理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、容器内存、序列化和性能都满足生产基线。
