Featured image of post Java 17 LTS 新特性深度解读:Records + Sealed + Pattern Matching + Text Blocks + 强封装

Java 17 LTS 新特性深度解读:Records + Sealed + Pattern Matching + Text Blocks + 强封装

Java 17 LTS(2021-09-14 发布)核心特性:Record 数据载体、Sealed Class 封闭类、Pattern Matching for instanceof 正式、Text Blocks 文本块、Switch 表达式、强封装 JDK 内部 + G1 正式成默认 GC

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
第三方 LTSBellSoft 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 模式默认 GCZGC 转正,让 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不能被继承也不能继承其他类(但可以实现接口)。

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 旧写法:30 行 POJO
public class UserPOJO {
    private final Long id;
    private final String name;
    public UserPOJO(Long id, String name) { this.id = id; this.name = name; }
    public Long getId() { return id; }
    public String getName() { return name; }
    // equals / hashCode / toString ... 一堆
}

// 新写法:1 行
public record User(Long id, String name) {}

// 使用
User u = new User(1L, "Alice");
System.out.println(u.name());           // Alice(不是 getName())
System.out.println(u);                 // User[id=1, name=Alice](自动 toString)

收益:减少 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) 显式穷举所有子类,编译器能做模式匹配穷尽性检查

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 旧写法:interface 公开给所有人实现,switch case 写不全编译时无法检测
public interface Shape { double area(); }

// 新写法:Shape 只能被 Circle/Rectangle/Triangle 实现
public sealed interface Shape permits Circle, Rectangle, Triangle {
    double area();
}
public record Circle(double radius) implements Shape {
    public double area() { return Math.PI * radius * radius; }
}
public record Rectangle(double width, double height) implements Shape {
    public double area() { return width * height; }
}
public record Triangle(double base, double height) implements Shape {
    public double area() { return 0.5 * base * height; }
}

收益:编译器保证 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 块作用域内可用,强转由编译器插入

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 旧写法
if (obj instanceof String) {
    String s = (String) obj;  // 显式 cast
    System.out.println(s.length());
}

// 新写法
if (obj instanceof String s) {  // 一步:检查 + 绑定 + cast
    System.out.println(s.length());
}

收益:单个 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 正式——三个双引号包围多行字符串,缩进规则自动:编译器把所有行的最小公共缩进当成"基础缩进"剥掉。

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 旧写法
String html = "<html>\n" +
              "    <body>Hello</body>\n" +
              "</html>";

// 新写法
String html = """
        <html>
            <body>Hello</body>
        </html>
        """;

收益: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 做穷尽性检查。

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 旧写法(语句)
String type;
switch (day) {
    case MONDAY:
    case TUESDAY:
    case WEDNESDAY:
    case THURSDAY:
    case FRIDAY: type = "工作日"; break;
    case SATURDAY:
    case SUNDAY: type = "周末"; break;
    default: type = "无效"; break;
}

// 新写法(表达式)
String type = switch (day) {
    case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "工作日";
    case SATURDAY, SUNDAY -> "周末";
    default -> "无效";
};

收益:从 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 显式开启。

1
2
# 启动参数:开洞允许反射访问 sun.nio.ch
java --add-opens java.base/sun.nio.ch=ALL-UNNAMED -jar MyApp.jar

收益: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:+UseZGCZGC 转正(JEP 377)
新增(正式)-XX:+UseShenandoahGCShenandoah 转正(OpenJDK 12 引入,17 完善)
改默认--illegal-access=denyJDK 内部 API 默认禁止反射
新增--add-opens <module>/<package>=<module>显式开洞允许反射
废弃-XX:+UseConcMarkSweepGC(CMS)Java 14 移除(JEP 363),17 已彻底消失
移除-Xbootclasspath/p:模块化后不再需要
废弃-XX:+PrintGCRoots 等调试参数改用 JFR(Java Flight Recorder)

启用 ZGC(正式版):

1
2
3
4
java -XX:+UseZGC -Xmx16g -jar MyApp.jar

# ZGC + 4TB 大堆场景(生产推荐)
java -XX:+UseZGC -XX:MaxGCPauseMillis=100 -Xmx2t -jar MyApp.jar

§4 垃圾回收器演进

GCJava 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 启用大堆):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# G1 调优:让 InitiatingHeapOccupancyPercent 提前到 30%
# (默认 45%)降低大堆下并发模式失败概率
java -XX:+UseG1GC -XX:InitiatingHeapOccupancyPercent=30 -Xmx8g MyApp

# ZGC 正式启用(4TB 大堆,暂停 < 10ms)
java -XX:+UseZGC -XX:+UseLargePages -Xmx4t MyApp

# G1 vs ZGC 选择决策
# - 中小堆(< 32GB):G1 默认即可,ZGC 收益不明显
# - 大堆(> 32GB)或极低延迟(< 50ms)要求:换 ZGC
# - 旧 CMS 迁移:先 G1 验证,再考虑 ZGC

§5 生产代码实战

Demo 1:Record + Sealed + Pattern Matching for instanceof

文件:assets/code/jdk-lts/Java-17/RecordAndSealedDemo.java

场景:电商订单里"商品"有多种形态(圆盘 / 矩形 / 三角形——比如不同形状的包装盒),需要根据商品类型计算面积总和。这是工业代码里"用 sealed interface 定义有限集合" + “Record 当 DTO” 的典型场景。

关键代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
sealed interface Shape permits Circle, Rectangle, Triangle {
    double area();
}
record Circle(double radius) implements Shape {
    public double area() { return Math.PI * radius * radius; }
}
record Rectangle(double width, double height) implements Shape {
    public double area() { return width * height; }
}
record Triangle(double base, double height) implements Shape {
    public double area() { return 0.5 * base * height; }
}

Shape[] shapes = { new Circle(5.0), new Rectangle(3, 4), new Triangle(3, 4) };
double totalArea = 0;
for (Shape s : shapes) {
    if (s instanceof Circle c) {            // Pattern Matching 正式
        totalArea += Math.PI * c.radius() * c.radius();
    } else if (s instanceof Rectangle r) {
        totalArea += r.width() * r.height();
    } else if (s instanceof Triangle t) {
        totalArea += 0.5 * t.base() * t.height();
    }
    // 因为 Shape 是 sealed,编译器保证 3 个 case 覆盖所有子类型
}

跑通命令

1
2
3
cd assets/code/jdk-lts/Java-17
javac --release 17 -Xlint:all RecordAndSealedDemo.java
java RecordAndSealedDemo

预期输出

1
2
3
4
Record toString: Circle[radius=5.0]
Record 自动 equals: true
Record 自动 hashCode: 1075052544
总面积(sealed + pattern matching): 96.53981633974483

为什么这个 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 模式都靠它)。

关键代码(旧新对比同文件):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 旧写法(Java 16 之前)
if (o instanceof String) {
    String s = (String) o;        // 显式 cast
    System.out.println("字符串长度: " + s.length());
} else if (o instanceof Integer) {
    Integer i = (Integer) o;       // 显式 cast
    System.out.println("整数 *2: " + (i * 2));
}

// 新写法(Java 17 正式)
if (o instanceof String s) {       // 一步:检查 + 绑定 + cast
    System.out.println("字符串长度: " + s.length());
} else if (o instanceof Integer i) {
    System.out.println("整数 *2: " + (i * 2));
}

跑通命令

1
2
3
cd assets/code/jdk-lts/Java-17
javac --release 17 -Xlint:all PatternMatchingRefactorDemo.java
java PatternMatchingRefactorDemo

预期输出: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 步走

  1. 依赖扫描:用 jdeps --jdk-internals MyApp.jar 列出所有反射访问 JDK 内部的代码
  2. 字节码确认javap -v MyApp.class | grep "major version" 看字节码版本
  3. JUnit 测试:JDK 17 跑测试套,捕获 InaccessibleObjectException 清单
  4. 灰度切流:单实例先跑 1 周,监控 GC 日志(java -Xlog:gc*)和启动参数是否缺 --add-opens
  5. 全量切:灰度无问题后改默认 JDK = 17 全量

回滚预案

  • 保留 JDK 11 路径(用 SDKMAN 切换或 docker 镜像 tag)
  • 启动参数加 -XX:+UseParallelGC 暂用旧默认 GC
  • 强封装问题用 --add-opens 临时开洞(只是临时方案,正式要升级库)
  • 单实例切流 1 周后再全量,回滚窗口 ~5 分钟(kill -9 + 切回 JDK 11 镜像)

§7 踩坑实录

  1. Record 不能被继承(隐式 final)—— 想用 User extends BaseUser 风格共享字段?Record 不允许。替代:用普通 class + Lombok @Value,或 Record 里嵌套一个父 Record(注意:嵌套 Record 不继承父 Record 字段访问器)。
  2. 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
  3. Pattern Matching 不能匹配 primitive 类型 —— if (obj instanceof int) 编译报错。objObject,要测 if (obj instanceof Integer i)绕路:先 if (obj instanceof Integer i) 再用 i.intValue()
  4. Text Blocks 缩进敏感 —— 三引号起始位置决定所有行最小缩进,缩进不到位的行会被截断。"""\n hello\n world\n """ 输出是 hello\nworld(前导空格被吃)。""" 闭合行必须独立——如果 """\n 后面还有 } 这种尾巴,编译器会认为是引号的一部分。
  5. Switch 表达式返回值必须所有分支都返回 —— 漏掉某个 case 又没有 default 会编译报错(即使在 sealed 接口下,编译器做穷尽性检查仍要求所有子类都覆盖)。
  6. 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 深度解读

本系列共 5 篇,本文为第 3 篇 · 查看全部
使用 Hugo 构建
主题 StackJimmy 设计