Skip to content

JDK17-21 语言新特性迁移指南 ​

面试常以「你们生产用什么 JDK 版本、用没用 record/sealed」切入,考的不只是语法,更是版本升级的工程判断力。本篇按 LTS 路线 + 五大特性(var / record / sealed / 模式匹配 / 文本块)展开。每个特性按「是什么 / 解决什么 / 代码对照 / 迁移注意」四段式给出。最后给企业迁移的现实考量。并发侧的 JDK21 大件见 虚拟线程 vs 线程池(JDK21),GC 侧的 JDK21 变化见 GC现代化-ZGC与Shenandoah。

1 LTS 路线与企业迁移的现实考量 ​

LTS发布语言/运行时代表特性生态基线
82014Lambda / Stream / 新时间日期企业存量主力
112018var;模块化落地;HTTP Client;移除 JAXB 等 JavaEE 模块Spring Boot 2 基线
172021record / instanceof 模式 / sealed 转正;强封装 JDK 内部 APISpring 6 / Boot 3 的最低基线
212023虚拟线程;switch 模式匹配;分代 ZGCSpring Boot 3.2+(虚拟线程开关)

现实考量(升级的阻力从来不是语法):

  • 强封装:JDK16 起 --illegal-access 默认拒绝,sun.misc.*、反射改 JDK 内部字段的库(老版本 Netty、Guava、各种字节码工具)直接抛 InaccessibleObjectException。--add-opens 只是续命,正路是升级依赖;
  • 命名空间:Spring 6 / Tomcat 10 起 javax.* → jakarta.*,全量 import 替换 + 生态联动升级;
  • 字节码基线:class 文件版本 17=61、21=65。ASM/CGLIB/反射字节码库必须升到支持对应版本的版本,否则编译期直接显示错误;
  • 被移除的 API:JAXB/JAX-WS(11 移除)、Nashorn、SecurityManager 弃用(17,JEP 411)都可能砸中老代码;
  • 策略上社区经验是逐 LTS 跳:8 → 17 灰度(先升三方包、CI 双跑、压测对比)→ 21,而不是一把跨 9 个版本。

2 var:局部变量类型推断 ​

  • 是什么:JDK10(JEP 286),局部变量声明用 var 交给编译器从右侧推断,静态类型不变,不是动态类型;
  • 解决什么:泛型容器声明的冗长重复。
java
Map<String, List<Order>> ordersByUser = new HashMap<>();   // 旧
var ordersByUser = new HashMap<String, List<Order>>();     // JDK10+

// 反例:右侧没有信息量时禁用
var result = service.process(input);   // result 是什么?可读性反而变差
  • 迁移注意:只能用于局部变量(含 for、try-with-resources),不能用于字段/方法参数/返回值。团队约定「右侧带泛型/构造器才用 var,接口类型语义不明时不用」。JDK11 起还可用 var 标注 lambda 参数以便加注解((@NonNull var s) -> ...)。

3 record:不可变数据载体 ​

  • 是什么:JDK16 转正(JEP 395),一种透明的不可变数据载体。隐式 final、组件全部 private final。自动生成 canonical 构造器、访问器(x() 而非 getX())、equals/hashCode/toString;
  • 解决什么:数据类的样板代码,并成为模式匹配(解构)的基础。
java
// JavaBean 写法:字段一多就是几十行样板
public class Point {
    private final int x;
    private final int y;
    public Point(int x, int y) { this.x = x; this.y = y; }
    public int getX() { return x; }
    public int getY() { return y; }
    public boolean equals(Object o) { /* 手写或 IDE 生成 */ return false; }
    public int hashCode() { return 0; }
    public String toString() { return ""; }
}

// record:一行,语义齐全且正确
public record Point(int x, int y) { }

与 Lombok / JavaBean 的取舍:

维度recordLombok(@Value/@Data)
实现层级语言级,编译器和模式匹配原生支持注解处理器,依赖 IDE/构建插件
可变性强制不可变可选可变(@Data 带 setter)
builder无(可手写紧凑构造器或静态工厂)@Builder 开箱即用
继承隐式 final、不能继承任何类普通类,可继承
版本要求JDK16+Java 8 即可用
  • 迁移注意:不是无脑替换 JavaBean——DTO、值对象、接口返回模型优先。需要可变领域实体、builder、JPA 实体(代理/懒加载需要非常 final)时 Lombok 仍合理。equals 是全组件比较,别与业务唯一键混淆。Jackson 需 2.12+ 才对 record 友好。紧凑构造器(compact constructor)可做参数校验,替代 @Builder 的一部分场景。

4 sealed:受控的继承 ​

  • 是什么:JDK17 转正(JEP 409),sealed + permits 限定哪些类可以继承/实现自己,子类必须显式声明 final、sealed 或 non-sealed;
  • 解决什么:在 final(彻底封闭)与默认开放之间给出第三档——受控的可扩展性。让编译器掌握完整的子类集合,为 switch 穷尽性检查铺路。
java
public sealed interface Shape permits Circle, Square, Rect {}
public record Circle(double r) implements Shape {}
public record Square(double side) implements Shape {}
public record Rect(double w, double h) implements Shape {}
  • 迁移注意:permits 的子类必须与父类同模块(未命名模块时同包)且直接实现。AOP/CGLIB 想给 sealed 类生成运行时子类会直接失败,框架代理场景要先确认。领域建模上「这段分支逻辑的所有情况」想固定下来时是最佳落点。

5 模式匹配:instanceof 与 switch 的演进 ​

instanceof 模式(JDK16,JEP 394):

java
// 旧:判断 + 强转 + 新变量,三步走
if (obj instanceof String) {
    String s = (String) obj;
    System.out.println(s.length());
}
// 新:判断与绑定一步完成,作用域内即用
if (obj instanceof String s) {
    System.out.println(s.length());
}

switch 表达式(JDK14,JEP 361)→ switch 模式匹配(JDK21,JEP 441)+ record 解构(JEP 440):

java
// switch 表达式:可作值、箭头语法无穿透、需穷尽
String dayType = switch (code) {
    case 1, 7 -> "周末";
    default   -> "工作日";
};

// JDK21:类型模式 + when 守卫 + record 解构,配合 sealed 可穷尽、免 default
double area(Shape s) {
    return switch (s) {
        case Circle c          -> Math.PI * c.r() * c.r();
        case Square q          -> q.side() * q.side();
        case Rect(double w, double h) when w == h -> w * w;  // 守卫先于下一行
        case Rect(double w, double h) -> w * h;
    };
}
  • 解决什么:消灭 if-else 强转链;穷尽性检查把「新增一个子类忘改分支」从运行时事故变成编译错误;
  • 迁移注意:传统 switch 传 null 会 NPE,模式 switch 可显式 case null。枚举/sealed 穷尽时省略 default 才能吃到「加新值编译错误」的红利,写了 default 反而失去保护。record 解构是嵌套的,复杂模式注意可读性。

6 文本块:多行字符串 ​

  • 是什么:JDK15 转正(JEP 378),""" 定义多行字符串;
  • 解决什么:SQL/JSON/HTML/脚本内嵌时的转义与换行地狱。
java
// 旧:转义地狱
String json = "{\n  \"name\": \"muyi\",\n  \"tags\": [\"java\"]\n}";
// 文本块
String json = """
        {
          "name": "muyi",
          "tags": ["java"]
        }""";
  • 迁移注意:开定界符后必须换行(首行换行不进字符串)。缩进按结尾 """ 的位置推导最小公共缩进剥离。行尾 \ 续行、\s 保留尾随空格。本质仍是普通 String(有转义、非「原始字符串」),别和字符串拼接性能问题混为一谈。同期值得顺手带的:helpful NullPointerException(14,精确指出哪个变量为 null)、String.isBlank/strip/lines、List.of/Map.of 不可变集合。

面试追问链 ​

  • var 是动态类型吗?→ 不是,编译期推断后类型固定,只是声明处省略;
  • record 和 Lombok 怎么选?→ 不可变 DTO/值对象用 record(语言级 + 模式匹配配合),要可变/builder/JPA 实体继续 Lombok;
  • record 能被继承吗?→ 不能,隐式 final 且父类固定为 java.lang.Record;可实现接口;
  • sealed 存在的意义?→ 在 final 与开放之间受控扩展,核心收益是编译器穷尽性检查;
  • switch 模式匹配的穷尽性怎么来的?→ sealed 子类集合编译期已知,漏分支编译错误;
  • 文本块怎么控制缩进和换行?→ 最小公共缩进剥离、\ 续行、\s 保尾空格;
  • 8→17 迁移踩过什么坑?→ 强封装 InaccessibleObjectException、javax→jakarta、ASM/CGLIB 版本、JAXB 移除,答题时举真实依赖名最加分。

面试官视角小结 ​

特性题的加分梯度:会念 API 是及格。能给出「record vs Lombok」「sealed + switch 穷尽」这类组合判断是良好。能把语言特性放进升级工程里谈——LTS 节奏、生态基线(Spring Boot 3 = 17)、强封装带来的依赖升级成本。以及「为什么 21 值得等(虚拟线程 + 分代 ZGC,见 GC现代化)」——才是面试官想要的工程视角。纯并发侧的版本收益另见 线程池设计思想 与虚拟线程篇。

2026-10 新增(P1 时效升级)。