Java 17 LTS 新特性深度解读:Records + Sealed + Pattern Matching + Text Blocks + 强封装
§1 版本元数据
| 项目 | 值 |
|---|---|
| 官方名称 | Java SE 17 |
| 发布日 | 2021-09-14 |
| 类型 | LTS(Long-Term Support) |
| Oracle Premier Support 截止 | 2027-09(已延后;公开免费 2024-09 结束) |
| Oracle Extended Support 截止 | 2029-09 |
| 第三方 LTS | BellSoft Liberica 至 2030-03 / Azul Zulu 至 2030 |
| 关键里程碑 | Records / Sealed / Pattern Matching 正式 + G1 正式默认 + ZGC 正式 + 强封装 JDK 内部 |
| JEP 总数 | 约 17 个 JEP(17uXX 累计更多) |
| 同期生态 | OpenJDK 17 + Eclipse Adoptium Temurin 17(免费生产可用) |
为什么 Java 17 是 Java 8 以来最值得升级的 LTS:Java 9-16 累积的特性(Records / Sealed / Pattern Matching / Text Blocks / Switch 表达式 / var)都在 Java 17 正式落地,配合 G1 正式成为 server 模式默认 GC 和 ZGC 转正,让 Java 17 成为真正可作为长期生产目标的版本。对于还在 Java 8 / Java 11 上的项目,Java 17 是从"老 LTS"切到"现代 LTS"的最自然节点——不必再冒险用 6 个月的非 LTS 中间版本(如 Java 16)。
§2 重大新特性
2.1 Record(JEP 395)—— 不可变数据载体
问题:写一个 POJO(普通 Java 对象)要 30 行——字段、构造器、getter、equals、hashCode、toString 全手写。Lombok 等工具能解决,但语言原生支持才是正路。Spring 生态里 DTO / VO / Domain Object 几乎全是 POJO,减一个字段就要改 5 个地方——维护成本极高。
方案:Record 是 Java 14 引入的透明数据载体——编译器根据字段声明自动生成构造器、访问器、equals、hashCode、toString。Record 隐式 final,不能被继承也不能继承其他类(但可以实现接口)。
代码:
| |
收益:减少 90% 模板代码;不可变(线程安全);自动 equals/hashCode 适合做 DTO / 领域模型 / Map 的 key / Jackson 序列化载体。但 Record 不能被继承(隐式 final),不能有可变字段——这是设计上的取舍。Jackson / Gson / Hibernate / Spring Data 都支持 Record(从 2.13 / 6.x 起),生产可直接用。
2.2 Sealed Class(JEP 409)—— 封闭类
问题:Java 类默认可被任何类继承,导致"基类被谁实现"完全失控——switch 模式匹配做穷尽性检查时也无从验证。Scala / Kotlin 的 sealed class / Rust 的 enum 早就解决了。
方案:sealed 关键字明确"哪些类可以继承/实现我",配合 permits 列出允许的子类。子类必须为 final / sealed / non-sealed 之一。Sealed 类在同一编译单元(同一 module / 同一 package) 显式穷举所有子类,编译器能做模式匹配穷尽性检查。
代码:
| |
收益:编译器保证 permits 列表之外没人能实现 Shape——配合 Pattern Matching 写"穷尽性 switch" 时漏 case 编译就报错。让 Java 第一次具备"代数数据类型"(Algebraic Data Type)能力,跟 Kotlin sealed class / Scala sealed trait / Rust enum 同源。领域驱动设计(DDD)的"值对象 + 有限状态机"在 Java 里第一次有了原生语法支持。
2.3 Pattern Matching for instanceof(JEP 394)—— 模式匹配正式
问题:instanceof 检查 + 显式 cast 是 Java 6 以来最啰嗦的模式之一——每条 if 分支要写两次类型名(一次检查、一次强转)。生产代码里这种 cast 模式有几十处,整体可减 30%-50% 噪音。
方案:Java 16 引入预览,Java 17 正式——instanceof 后面直接声明绑定变量,编译器自动 cast。绑定变量在 if 块作用域内可用,强转由编译器插入。
代码:
| |
收益:单个 instanceof 减少 2 行 boilerplate;老的 if (x instanceof Type) { Type t = (Type) x; ... } 模式写几十处时整体可减 30%-50% 噪音。生产代码里这种 cast 模式到处都是——DTO 反序列化、Visitor 模式、equals 方法都从中受益。注意:绑定变量不能用在 && 右侧(流分析的限制)—— if (obj instanceof String s && s.length() > 5) 是 OK 的,但 if (obj instanceof String s || s.length() > 5) 会编译失败(s 可能在 || 右侧未初始化)。
2.4 Text Blocks(JEP 378)—— 多行字符串
问题:Java 字符串不支持多行字面量——写 SQL、JSON、HTML 都要 "a" + "b" + "c" 拼接,可读性差且转义字符爆炸。Kotlin / Scala / Python 早就有原生多行字符串。
方案:Java 13 引入、Java 15 正式——三个双引号包围多行字符串,缩进规则自动:编译器把所有行的最小公共缩进当成"基础缩进"剥掉。
代码:
| |
收益:HTML / SQL / JSON 字面量从 8 行降到 5 行;缩进自动对齐;但缩进敏感(坑见 §7)。写单元测试 assertion 字符串特别爽——以前要 \"a\\nb\\nc\" 转义,现在直接多行。
2.5 Switch 表达式(JEP 361)—— 表达式化
问题:传统 switch 是语句——每个 case 写 break;、变量赋值要预先声明。漏 break 写出 bug 是 Java 经典陷阱(“fall-through 故意 vs 意外”)。Java 14 引入 switch 表达式预览,Java 17 正式。
方案:switch 升级为表达式,直接返回值。-> 语法(不需要 break)+ 多值匹配(case A, B, C ->)+ 配合 Sealed 做穷尽性检查。
代码:
| |
收益:从 10 行 + break 陷阱降到 4 行;多个 case 共享同一结果用 , 简写;switch 表达式 + Sealed 配合能编译期检测"漏 case"(漏 case 编译报错)。注意:箭头语法 -> 不写 break 也不会 fall-through——这跟传统 : 写法的关键区别。
2.6 强封装 JDK 内部(JEP 403)—— --illegal-access=deny 默认
问题:Java 9 模块化后,sun.misc.Unsafe / sun.nio.ch.* 等 JDK 内部 API 仍被反射广泛使用——setAccessible(true) 一开就绕过封装。Spring / Hibernate / Netty / Mockito / ByteBuddy 等知名库在 Java 9 时代频繁反射 JDK 内部。
方案:Java 16 起 --illegal-access=permit(默认),Java 17 改为 --illegal-access=deny 默认——所有内部 API 默认不可反射访问。需要显式 --add-opens java.base/sun.nio.ch=ALL-UNNAMED 开启。
代码:需用 --add-opens 显式开启。
| |
收益:JDK 内部 API 真正封装了,老库(ByteBuddy / Mockito / Netty)要更新到新版本才能跑在 Java 17 上。这是 enterprise 升级 Java 17 的最大坑——很多老代码用反射访问 sun.misc.BASE64Encoder 之类,Java 17 直接抛 InaccessibleObjectException。升级前先用 JDK 17 跑 JUnit 测试,捕获所有 InaccessibleObjectException,列清单逐个 --add-opens 或升级库。
§3 JVM 参数变更
| 变更类型 | 参数 | 说明 |
|---|---|---|
| 改默认 | 默认 GC = G1(正式默认,Java 9-16 是过渡期) | server 模式全部默认 G1 |
| 新增(正式) | -XX:+UseZGC | ZGC 转正(JEP 377) |
| 新增(正式) | -XX:+UseShenandoahGC | Shenandoah 转正(OpenJDK 12 引入,17 完善) |
| 改默认 | --illegal-access=deny | JDK 内部 API 默认禁止反射 |
| 新增 | --add-opens <module>/<package>=<module> | 显式开洞允许反射 |
| 废弃 | -XX:+UseConcMarkSweepGC(CMS) | Java 14 移除(JEP 363),17 已彻底消失 |
| 移除 | -Xbootclasspath/p: 等 | 模块化后不再需要 |
| 废弃 | -XX:+PrintGCRoots 等调试参数 | 改用 JFR(Java Flight Recorder) |
启用 ZGC(正式版):
| |
§4 垃圾回收器演进
| GC | Java 17 状态 | 引入版本 | 关键调优 |
|---|---|---|---|
| Serial | 保留 | 1.0 | -XX:+UseSerialGC |
| Parallel | 保留(不再是默认) | 1.4 | -XX:+UseParallelGC |
| G1 | 正式默认(Java 9-16 过渡,17 确立) | 7u4 | -XX:MaxGCPauseMillis=200 |
| ZGC | 正式(JEP 377) | 11 实验 → 15 正式 | -XX:+UseZGC |
| Shenandoah | 正式 | 12 | -XX:+UseShenandoahGC |
| Epsilon | 保留 | 11 | -XX:+UseEpsilonGC |
| CMS | 已移除(Java 14 起被删) | 1.4.1 | 不再可用 |
重点:
- G1 正式成 server 模式默认 GC(Java 9 起过渡,17 正式确立)—— 这是 Java 9-16 时期"实验性默认 G1"政策的最终落地
- ZGC 转正(JEP 377),不再是实验性——支持 16TB 大堆,暂停 < 1ms
- CMS 移除(JEP 363)—— 还在用 CMS 的 enterprise 项目必须升级到 G1 或 ZGC
调优示例(G1 让混合收集更早触发 + ZGC 启用大堆):
| |
§5 生产代码实战
Demo 1:Record + Sealed + Pattern Matching for instanceof
文件:assets/code/jdk-lts/Java-17/RecordAndSealedDemo.java
场景:电商订单里"商品"有多种形态(圆盘 / 矩形 / 三角形——比如不同形状的包装盒),需要根据商品类型计算面积总和。这是工业代码里"用 sealed interface 定义有限集合" + “Record 当 DTO” 的典型场景。
关键代码:
| |
跑通命令:
| |
预期输出:
| |
为什么这个 demo 有工业价值:
- 展示 Record 自动生成 toString/equals/hashCode(4 行替代 30 行)
- 展示 Sealed 限制实现类(未来加 Square 子类必须出现在 permits 列表)
- 展示 Pattern Matching 跟 Sealed 配合做编译期穷尽性检查
- 这是Spring 领域模型 + DDD 值对象 + 状态机的核心范式
Demo 2:Pattern Matching for instanceof 旧新对比
文件:assets/code/jdk-lts/Java-17/PatternMatchingRefactorDemo.java
场景:通用对象处理——Object[] 里混合字符串、整数、双精度、布尔、null、数组,分别处理。这是工业代码里"类型分发"的常见模式(事件处理、消息路由、Visitor 模式都靠它)。
关键代码(旧新对比同文件):
| |
跑通命令:
| |
预期输出:6 个对象各 1 行(“字符串长度: 5” / “整数 *2: 84” / “其他: 3.14” / “其他: true” / “null” / “数组长度: 3”),旧新两组共 12 行。
收益:每个 instanceof 链减少 2 行 boilerplate;老代码 if (x instanceof Type) { Type t = (Type) x; ... } 模式写几十处时整体可减 30%-50% 噪音。生产代码里 Visitor 模式、equals 方法、事件分发都从中受益。
§6 升级指南
从 Java 11 升 Java 17:
| 风险 | 缓解 |
|---|---|
强封装 JDK 内部(JEP 403)—— 反射访问 sun.misc.* 抛 InaccessibleObjectException | 加 --add-opens java.base/sun.nio.ch=ALL-UNNAMED 显式开洞;或升级到支持新模块的库版本(ByteBuddy 1.12+、Mockito 4+、Netty 4.1.65+、Hibernate 6+) |
| CMS GC 移除(JEP 363) | 启动参数去掉 -XX:+UseConcMarkSweepGC,改用 G1 或 ZGC |
| 默认 GC 从 Parallel 改 G1 | 若依赖 Parallel 行为,加 -XX:+UseParallelGC 显式指定 |
| 字节码版本 55 → 61 | 老 .class 文件不需重新编译,但运行时要求升 JDK 17+ |
| Text Blocks / Records / Sealed 跟旧 API 互操作 | 业务代码层不影响;调用方代码不识别 Record 时反射仍能拿 recordComponent 字段 |
| Spring Boot 2.4+ 默认要求 Java 11+ | 升级 Spring Boot 到 2.7+;Spring Boot 3.x 直接要 Java 17+ |
| 第三方库用 Nashorn(Java 15 移除) | 改用 GraalVM JavaScript |
升级 5 步走:
- 依赖扫描:用
jdeps --jdk-internals MyApp.jar列出所有反射访问 JDK 内部的代码 - 字节码确认:
javap -v MyApp.class | grep "major version"看字节码版本 - JUnit 测试:JDK 17 跑测试套,捕获
InaccessibleObjectException清单 - 灰度切流:单实例先跑 1 周,监控 GC 日志(
java -Xlog:gc*)和启动参数是否缺--add-opens - 全量切:灰度无问题后改默认 JDK = 17 全量
回滚预案:
- 保留 JDK 11 路径(用 SDKMAN 切换或 docker 镜像 tag)
- 启动参数加
-XX:+UseParallelGC暂用旧默认 GC - 强封装问题用
--add-opens临时开洞(只是临时方案,正式要升级库) - 单实例切流 1 周后再全量,回滚窗口 ~5 分钟(kill -9 + 切回 JDK 11 镜像)
§7 踩坑实录
- Record 不能被继承(隐式 final)—— 想用
User extends BaseUser风格共享字段?Record 不允许。替代:用普通 class + Lombok @Value,或 Record 里嵌套一个父 Record(注意:嵌套 Record 不继承父 Record 字段访问器)。 - Sealed 子类必须 final / sealed / non-sealed 之一 ——
record Circle(double r) implements Shape是 final(record 隐式 final),OK;但class Circle implements Shape默认是 package-private final 风格,必须显式加final/sealed/non-sealed。 - Pattern Matching 不能匹配 primitive 类型 ——
if (obj instanceof int)编译报错。obj是Object,要测if (obj instanceof Integer i)。绕路:先if (obj instanceof Integer i)再用i.intValue()。 - Text Blocks 缩进敏感 —— 三引号起始位置决定所有行最小缩进,缩进不到位的行会被截断。
"""\n hello\n world\n """输出是hello\nworld(前导空格被吃)。坑:"""闭合行必须独立——如果"""\n后面还有}这种尾巴,编译器会认为是引号的一部分。 - Switch 表达式返回值必须所有分支都返回 —— 漏掉某个 case 又没有 default 会编译报错(即使在 sealed 接口下,编译器做穷尽性检查仍要求所有子类都覆盖)。
- Sealed 跨 module 报错 ——
sealed interface Shape permits com.example.Circle跨 module 引用时,Circle 必须non-sealed或同 module 下 final/sealed。生产里建议:Sealed 层次放在同一 module / 同一 package,避免跨 module 引用造成编译错。
工业实践补充
真实项目里的 Java 17 升级路径(金融/电商/政企典型场景):
- 阶段 1(1-2 月):评估期。
jdeps --jdk-internals扫描所有 JAR 包,输出"哪些第三方库反射 JDK 内部"清单。常用工具:jdeps(JDK 自带)、OpenRewrite(自动迁移)、jdeprscan(检查废弃 API)。 - 阶段 2(2-3 月):升级期。先把构建工具(Maven / Gradle)目标改成 JDK 17,编译期先暴露所有"老语法"问题;然后跑测试套,捕获
InaccessibleObjectException清单;最后用--add-opens临时开洞,等库升级后再移除。 - 阶段 3(1-2 月):切流期。单实例跑 1 周监控 GC 日志(
java -Xlog:gc*=info:file=gc.log),对比 Java 11 的 GC 表现。关键监控指标:G1 暂停时间(应 < 200ms)、堆使用率(混合收集频率)、CPU 使用率(G1 并发线程开销)。 - 阶段 4(全量):全量切换。保留 JDK 11 镜像 tag 至少 1 个月,回滚窗口 < 5 分钟(kill -9 + 切回旧镜像)。
Java 17 性能数据(基于 OpenJDK 官方 benchmark + 业内真实数据):
- G1 vs Parallel GC:Java 17 G1 在 8GB 堆下比 Java 11 G1 暂停时间短 20-30%(G1 优化 JEP 344/346/348 等累积效果)。
- ZGC vs G1:16GB 堆下 ZGC 平均暂停 < 1ms,G1 平均暂停 50-200ms。但 ZGC 在 16GB 以下堆无明显优势(吞吐量比 G1 低 5-10%),ZGC 是大堆 + 极低延迟专用。
- Record 序列化:Jackson 2.13+ 支持 Record,反射开销跟普通 class 持平(Jackson 走 recordComponent 反射)。
- Sealed interface 性能:JVM 对 Sealed 优化使 instanceof 检查比传统 interface 快 5-10%(编译器可内联类型判断)。
Spring Boot 3.0 + Java 17 迁移(2022-11 Spring Boot 3.0 GA):
- Spring Boot 3.0 要求 Java 17+(最低版本从 Java 8 升到 17)
- Spring Boot 3.0 要求 Spring Framework 6.0+(基于 Jakarta EE 9,命名空间从
javax.*改jakarta.*) - Spring Boot 2.7 是最后一个支持 Java 8/11 的版本(2023-11 EOL)
- 如果项目在 Spring Boot 2.x 上,升级到 Java 17 需要:先升级 Spring Boot 2.7.x(兼容 Java 17)→ 再考虑升 3.0+
§8 下一篇预告
下一站 Java 21(2023-09-19 发布)—— 虚拟线程(JEP 444)正式 + 分代 ZGC(JEP 439)正式 + 序列集合(JEP 431)+ Switch 模式匹配(JEP 441)+ Record Patterns(JEP 440)。Java 21 是 Java 8 以来最值得升级的 LTS——虚拟线程让 Java 在高并发场景下重新具备竞争力。详见 Java 21 LTS 深度解读。
