Java新特性(四):Java 16 ~ 17(LTS)
Java新特性(四):Java 16 ~ 17(LTS)
导语:Java 17 是继 8、11 之后的第三个 LTS,也是目前企业升级最主流的目标版本。它把三个「预览了好几轮」的语言特性一次性转正——record、密封类、
instanceof模式匹配,同时开启了「强封装 JDK 内部 API」,直接决定了很多老项目的升级方式。本篇围绕这些必问点,并附一份 Java 8 → 17 升级清单。
一、record:不可变数据载体
1. record 和普通类有什么区别?(必问)
答: record(JEP 395,Java 14 预览、Java 16 转正)是不可变数据的透明载体,用一行代码替代过去的「字段 + 构造器 + getter + equals/hashCode/toString」样板代码。
// 一行搞定
public record Point(int x, int y) { }
// 编译器自动生成:
// 1) private final 字段 x、y
// 2) 全参构造器 Point(int x, int y)
// 3) 访问器 x()、y()(注意:不是 getX(),没有 get 前缀)
// 4) equals/hashCode(按所有组件值比较)
// 5) toString:Point[x=1, y=2]public class RecordDemo {
public static void main(String[] args) {
Point p = new Point(1, 2);
System.out.println(p); // Point[x=1, y=2]
System.out.println(p.x() + p.y()); // 3
System.out.println(p.equals(new Point(1, 2))); // true
System.out.println(p.hashCode() == new Point(1, 2).hashCode()); // true
}
}与普通类的关键差异:
| 维度 | record | 普通类 |
|---|---|---|
| 继承 | 隐式继承 java.lang.Record,不能再继承其它类,也不能被继承(隐式 final) | 自由 |
| 接口 | 可以实现接口 | 可以实现接口 |
| 字段 | 组件字段隐式 private final | 自定义 |
| setter | 没有,不可变 | 可自定义 |
| 访问器 | x(),无 get 前缀 | 习惯 getX() |
| 额外成员 | 可加静态字段/静态方法/实例方法/构造器,不能加实例字段 | 无限制 |
// 可以:实现接口、自定义方法、紧凑构造器
public record Range(int low, int high) implements Comparable<Range> {
// 紧凑构造器:只写参数校验/规范化,字段赋值由编译器自动补
public Range {
if (low > high) {
throw new IllegalArgumentException("low > high");
}
low = Math.max(low, 0); // 可以对参数做规范化
}
// 自定义实例方法
public int length() { return high - low; }
// 静态方法也允许
public static Range of(int low, int high) { return new Range(low, high); }
@Override
public int compareTo(Range o) { return Integer.compare(low, o.low); }
}2. 「record 是不可变的」——这句话有坑吗?(高频追问)
答: 有。record 只保证浅不可变(shallow immutability):
- 组件的引用是
final,不能被重新指向别的对象; - 但如果组件本身是可变对象(数组、
List、Date、自定义可变类),其内容仍然可以被修改。
record Order(String id, List<String> items) { }
List<String> items = new ArrayList<>(List.of("apple"));
Order order = new Order("o1", items);
order.items().add("banana"); // ⚠️ 合法!record 内部状态被改了
System.out.println(order); // Order[id=o1, items=[apple, banana]]正确做法:在紧凑构造器里做防御性拷贝,并返回不可变视图。
public record Order(String id, List<String> items) {
public Order {
// 存副本(1. 参数为 null 时也要处理)
items = List.copyOf(items); // 不可变拷贝,一劳永逸
}
}
// 数组要显式 clone
public record Buffer(byte[] data) {
public Buffer {
data = data.clone();
}
@Override
public byte[] data() { return data.clone(); } // 返回时也要拷贝
}3. record 与 Lombok 的 @Data / @Value 怎么选?
答:
- record 是语言级特性:编译期保证、无注解处理器、无额外依赖、IDE/调试器原生支持;
- Lombok 更灵活:支持可变字段、
@Builder、@AllArgsConstructor自由组合、能用于 JPA 实体(需要无参构造器 + setter); - JPA/Hibernate 实体不建议用 record(因为需要无参构造器 + 可变状态,除非用 Hibernate 6 的 record 支持场景)。
- 两者可共存:DTO/VO/事件对象用 record,实体用 Lombok 或手写。
record UserDTO(Long id, String name) { } // 适合:接口出入参、消息、查询结果加分点:record 也是「模式匹配」的基础。Java 21 起可以解构:
if (obj instanceof Point(int x, int y)),见后续《Java新特性(五)》。
二、密封类:把继承关系锁死
4. 密封类(Sealed Classes)是什么?(必问)
答: 密封类(JEP 409,Java 15 预览、Java 17 转正)用 permits 显式列出允许继承/实现的子类型,把「谁能扩展我」从「人人可继承」变成「白名单」。
作用:
- 建模封闭的类型层次(代数数据类型 ADT 的思路),让子类集合可控、可推理;
- 与 switch 穷尽性检查配合:sealed 类型的子类已知,switch 可以不写
default而由编译器保证全覆盖; - 提升 API 的可维护性:防止外部随意继承导致 API 兼容性被破坏。
// 密封接口:只允许白名单内的类型实现
public sealed interface Shape permits Circle, Rectangle, Triangle, Polygon { }
// 子类必须三选一:final(终止继承)、sealed(继续密封)、non-sealed(重新开放继承)
public record Circle(double radius) implements Shape { } // record 隐式 final
public record Rectangle(double width, double height) implements Shape { }
public non-sealed class Triangle implements Shape { } // 允许被任意继承
// 子类可以继续密封,并再次用 permits 限定
public sealed class Polygon implements Shape permits Square, Hexagon { }
public final class Square extends Polygon { }
public final class Hexagon extends Polygon { }规则的四个要点:
permits列出的类型必须在同一模块(未命名模块则同一包)内,且必须直接继承/实现该密封类型;- 每个 permitted 子类必须显式声明为
final、sealed或non-sealed(漏写会编译错误); - 如果子类与父类在同一个源文件中,可以省略
permits(编译器自动推断); - 密封类的构造器不能是
public(否则外部可绕过白名单直接实例化父类),通常隐式private或 package-private。
// 与 switch 穷尽性结合(Java 21 模式匹配 switch)
double area = switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
case Triangle t -> 0.0; // sealed 保证没有别的可能,无需 default
};面试常见对比:密封类不是「不能继承」,而是「只允许声明的类型继承」;它比
final更灵活(final 完全不能继承)、比普通类更严格(普通类谁都能继承)。
三、instanceof 模式匹配
5. instanceof 模式匹配解决什么问题?(必问)
答: 解决「判断类型 + 强制转换」的重复代码(JEP 394,Java 14 预览、Java 16 转正):
// 以前:判断一次,转换一次,变量作用域还得自己控制
if (obj instanceof String) {
String s = (String) obj;
System.out.println(s.length());
}
// 现在:模式变量一步到位
if (obj instanceof String s) {
System.out.println(s.length()); // s 已自动转换并赋值
}
// 好处:短路逻辑里也能用
if (obj instanceof String s && s.length() > 3) { // s 在 && 右侧已可见
System.out.println(s.toUpperCase());
}
// 取反也安全(模式变量在条件为 false 时不可用,编译器帮你把关)
if (!(obj instanceof String s)) {
return;
}
System.out.println(s.length()); // 这里 s 已确定可用(流敏感的作用域)作用域规则(考点):
- 模式变量只在「模式一定匹配成功」的位置可见(靠编译器的流分析确定);
- 与
||、&&、三元运算符、return/throw组合时,可见范围会相应扩大或缩小; - 模式变量不是 final,但重新赋值它会被视为语法错误?——实际允许赋值,不过不推荐(JDK 16 起模式变量不再是隐式 final)。
四、Java 16 的其他重点
6. Stream.toList() 与 collect(Collectors.toList()) 有何区别?
答: Java 16 新增 Stream.toList(),写法更短,但返回的是不可变 List:
List<String> a = stream.collect(Collectors.toList()); // 通常可变(实现不保证)
List<String> b = stream.toList(); // 不可变,试图修改抛 UnsupportedOperationException
// 需要可变结果时
List<String> c = new ArrayList<>(stream.toList());| 维度 | collect(Collectors.toList()) | Stream.toList() |
|---|---|---|
| 长度 | 冗长 | 简洁 |
| 可变性 | 实现相关(一般可变) | 不可变 |
| 是否允许 null 元素 | 允许 | 允许(toList 允许 null,这点与 List.of 不同,注意区分) |
追加考点:
Collectors.toUnmodifiableList()返回不可变且不允许 null,三者行为各异,面试常拿来对比。
7. 其他 Java 16 要点
答:
- 默认强封装 JDK 内部 API(JEP 396):
--illegal-access默认值由permit变为deny,直接反射java.*/sun.*内部类会抛InaccessibleObjectException;Java 17(JEP 403)进一步取消--illegal-access选项,只留下--add-opens/--add-exports作为出口。 jpackage转正(JEP 392):平台安装包工具从孵化毕业。- Unix-Domain Socket Channels(JEP 380):
java.nio支持 AF_UNIX 套接字,本机进程间通信比 TCP 回环更快、更安全。 - Elastic Metaspace(JEP 387):类元数据内存按需归还操作系统,碎片更少、占用更低。
- ZGC 并发线程栈处理(JEP 376):把最后的停顿源也并发化,ZGC 停顿进一步逼近毫秒级。
- 基于值的类警告(JEP 390):对
Integer、LocalDate等「基于值的类」在同步块中使用会告警(因为它们未来可能变成值类型,身份语义不可靠)。 - Vector API 首次孵化(JEP 338):
jdk.incubator.vector,让 Java 显式使用 SIMD 指令。 - 平台扩展:Alpine Linux/musl 移植(JEP 386)、Windows/AArch64 移植(JEP 388)。
五、Java 17:LTS 的其他重点
8. Java 17 除了转正,还改了什么?
答:
- 密封类转正(JEP 409)、
instanceof模式匹配转正(JEP 394)、record转正(JEP 395)——都已在前文展开。 - switch 模式匹配首次预览(JEP 406):
case Integer i ->、case null ->、守卫when正式登场,Java 21 转正。 - 恢复「始终严格」的浮点语义(JEP 306):
strictfp成为历史(该关键字在 Java 17 起被标记为无作用)。原因是 1990 年代 x87 浮点协处理器的扩展精度问题已不复存在,统一按 IEEE 754 严格语义执行。 - 增强的伪随机数生成器(JEP 356):引入
RandomGenerator接口族(RandomGeneratorFactory、SplittableRandom、Xoroshiro128PlusPlus、L64X128MixRandom等),支持多种算法与Splittable/Jumpable/Leapable能力,可按需选「更高质量」或「更快」的算法。
import java.util.random.RandomGenerator;
import java.util.random.RandomGeneratorFactory;
RandomGenerator g = RandomGenerator.of("L128X256MixRandom"); // 按算法名创建
int i = g.nextInt(100);
boolean dice = g.nextBoolean();
// 列出本 JDK 支持的所有算法
RandomGeneratorFactory.all()
.map(RandomGeneratorFactory::name)
.forEach(System.out::println);- 移除实验性的 AOT 与 JIT 编译器(JEP 410):
jaotc、Graal JIT 从 JDK 移除(GraalVM 独立演进);JDK 24 起以 AOT 缓存(Project Leyden) 的新形态回归,别混淆。 - 移除 RMI Activation(JEP 407)、弃用 Security Manager(JEP 411)(Java 24 永久禁用)。
- 上下文特定的反序列化过滤器(JEP 415):
ObjectInputFilter可按上下文配置多个过滤器,是反序列化漏洞治理的官方手段。 - FFM API 首次孵化(JEP 412)、Vector API 第二次孵化(JEP 414)。
- 新的 macOS 渲染管线(JEP 382)、macOS/AArch64 端口(JEP 391)。
9. 从 Java 8 升级到 17 / 21 需要注意什么?(必问)
答: 分「不兼容变更」和「运行时影响」两类。
① 强封装 JDK 内部 API(最容易踩)
# 旧代码反射访问 sun.misc.Unsafe、java.lang 内部类时会抛:
# java.lang.reflect.InaccessibleObjectException: Unable to make field ... accessible
# 解决:显式开放
java --add-opens java.base/java.lang=ALL-UNNAMED \
--add-exports java.base/sun.nio.ch=ALL-UNNAMED \
-jar app.jar② 移除的模块与 API:JDK 11 移除 Java EE / CORBA(javax.xml.ws、javax.corba 等);Nashorn(15 移除);CMS(14 移除)。
③ javax.* → jakarta.* 要说清楚:这是 Jakarta EE 9 的包名迁移(Servlet、JPA、Validation 等),与 JDK 升级是两件事。Spring Boot 3 同时要求 JDK 17+ 和 Jakarta EE 9+,所以升级 Spring Boot 3 时两件事会同时发生,面试时不要混为一谈。
④ GC 与 JVM 参数变化:默认 GC 从 Parallel(8)变为 G1(9+);-XX:+UseConcMarkSweepGC 参数在 14+ 直接失效;日志参数统一为 -Xlog;偏向锁在 15 起默认关闭。
⑤ 编译与依赖工具链:
# 推荐:用 --release 而非 -source/-target,避免「编译通过但运行 NoSuchMethodError」
javac --release 17 -d out src/**/*.java
# 检查项目对 JDK 内部 API 的依赖
jdeps --jdk-internals -recursive target/app.jar⑥ 参数校验类的常见报错:IllegalAccessError(模块未导出)、NoClassDefFoundError(EE 模块被移除)、UnsupportedClassVersionError(字节码版本不匹配)、InaccessibleObjectException(反射被拒)——能说出这几个报错与对应原因,基本就答满了。
面试模板回答:「升级 Java 版本主要看四件事:语言/API 兼容性(用
--release+jdeps扫)、JDK 内部 API 强封装(--add-opens兜底)、被移除的 EE 模块与 GC 参数、以及依赖框架的版本要求(如 Spring Boot 3 要求 17+ 且迁到jakarta.*)。」
六、小结
| 特性 | Java 14 | Java 15 | Java 16 | Java 17 |
|---|---|---|---|---|
instanceof 模式匹配 | 预览 | 预览 | ✅ 转正 | —— |
record | 预览 | —— | ✅ 转正 | —— |
| 密封类 | —— | 预览 | 预览 | ✅ 转正 |
| switch 模式匹配 | —— | —— | —— | 预览(21 转正) |
| Helpful NPE | ✅ | 默认开启 | —— | —— |
| 强封装 JDK 内部 API | —— | —— | 默认 deny | 取消 --illegal-access |
