Java 17 在语言、核心库、JVM 和平台支持等多个维度都带来了重要更新,也是目前 Spring Boot 3.x 等主流框架的最低 JDK 基线。
一、语言语法特性
1. 密封类(Sealed Classes)
Java 17 之前,类的继承控制只有两种极端: final——完全禁止继承; 不加final——任何类都能继承。 而密封类提供了第三种状态:只允许我指定的几个类继承。即,可以明确指定“只有这几个类能继承我”,其他任何类尝试继承都会在编译时报错。
声明密封类:
使用 sealed 修饰符,并通过 permits 子句列出允许继承的子类.
// 密封接口
public sealed interface Shape permits Circle, Rectangle, Square {
double area();
}
// 密封类
public abstract sealed class Shape permits Circle, Rectangle, Square {
protected String color;
public Shape(String color) { this.color = color; }
public abstract double area();
}
关键约束规则:
- 同模块/同包约束:permits 中的子类必须与密封类在同一个模块中;若在未命名模块中,则必须在同一个包中。
- 直接继承:子类必须直接继承密封父类,不能跳过层级;
- 必须三选一:子类必须是 final、sealed 或 non-sealed,不能组合使用;
final:不能再被继承sealed:继续限制继承(需要自己的permits子句)non-sealed:解除限制,允许任意类继承
- 至少一个子类:密封类必须至少有一个许可子类,否则编译报错。
// ========== 密封类定义 ==========
public sealed class Shape permits Circle, Square, Rectangle {
protected String color;
public Shape(String color) { this.color = color; }
public String getColor() { return color; }
}
// ========== 允许的子类 ==========
// final:不能再被继承
public final class Circle extends Shape {
private double radius;
public Circle(String color, double radius) {
super(color);
this.radius = radius;
}
public double area() { return Math.PI * radius * radius; }
}
// non-sealed:解除限制,允许任意类继承
public non-sealed class Square extends Shape {
private double side;
public Square(String color, double side) {
super(color);
this.side = side;
}
public double area() { return side * side; }
}
// sealed:继续限制继承,只允许 FilledRectangle 继承
public sealed class Rectangle extends Shape permits FilledRectangle {
private double length, width;
public Rectangle(String color, double length, double width) {
super(color);
this.length = length;
this.width = width;
}
public double area() { return length * width; }
}
// FilledRectangle 是 Rectangle 的唯一允许子类
public final class FilledRectangle extends Rectangle {
private int red, green, blue;
public FilledRectangle(String color, double length, double width,
int red, int green, int blue) {
super(color, length, width);
this.red = red; this.green = green; this.blue = blue;
}
}
省略 permits 的写法:
当子类数量少且精简时,可以将所有子类写在同一个源文件中,此时编译器能自动推断,permits 子句可以省略。
// 所有子类都在同一个文件中,permits 可省略
public sealed class Figure { }
final class Circle extends Figure { float radius; }
non-sealed class Square extends Figure { float side; }
与模式匹配switch的协同 / 与Record结合:
密封类最重要的价值体现在与模式匹配 for switch 的配合上。因为编译器知道所有可能的子类,switch 可以做到“穷尽性检查”,不需要 default 分支。
public static String describe(Shape shape) {
return switch (shape) {
case Circle c -> "圆形,半径 " + c.getRadius();
case Square s -> "正方形,边长 " + s.getSide();
case Rectangle r -> "矩形,尺寸 " + r.getLength() + "x" + r.getWidth();
// 不需要 default!编译器知道所有子类都已覆盖
};
}
record 类隐式 final,天然适合作为密封类的叶子节点。
public sealed interface Expr permits ConstantExpr, PlusExpr, NegExpr { }
public record ConstantExpr(int value) implements Expr { }
public record PlusExpr(Expr left, Expr right) implements Expr { }
public record NegExpr(Expr operand) implements Expr { }
密封类把 Java 的继承从"要么全开、要么全关"变成了精细化的白名单机制,配合 switch 模式匹配的穷尽性检查,让类型系统的表达能力和安全性都上了一个台阶。
2. switch 模式匹配
switch 模式匹配是一个预览特性,旨在让 switch 能够基于对象的类型进行匹配,并直接提取数据,从而取代冗长的if-else instanceof链。它让 switch 不仅能匹配常量值,还能直接匹配类型、null 和带条件的模式,并在 switch 表达式中配合密封类/枚举进行穷尽性检查,从而替代大量 instanceof + 强制转换代码。
类型模式匹配
// Java 17 之前
String format(Object obj) {
if (obj instanceof String) {
return "字符串: " + ((String) obj).length();
} else if (obj instanceof Integer) {
return "整数: " + ((Integer) obj);
}
return "未知";
}
// Java 17(预览特性)
String format(Object obj) {
return switch (obj) {
// case String s 同时完成类型判断和变量绑定,无需手动强转。
case String s -> "字符串,长度: " + s.length();
case Integer i -> "整数: " + i;
case Double d -> "浮点数: " + d;
case null -> "空值";
default -> "未知类型: " + obj.getClass().getSimpleName();
};
}
null 安全处理
传统 switch 遇到 null 会直接抛 NullPointerException,Java 17 允许显式匹配 null:
String describe(Object obj) {
return switch (obj) {
case null -> "传入的是 null";
case String s -> "字符串: " + s;
default -> "其他类型";
};
}
// 如果你不写case null,而 o 为null,switch 的行为与传统一致,仍然会抛出NullPointerException。
保护模式
你可以使用 && 在类型模式后附加一个布尔表达式(守卫),进一步细化匹配条件。只有当守卫表达式为 true 时,该分支才会匹配:
// 假设有一个密封接口 Shape 及其实现 Circle、Rectangle
static void classify(Shape s) {
System.out.println(switch (s) {
case Circle c && c.area() < 100.0 -> "Small Circle: " + c;
case Circle c -> "Large Circle: " + c;
case Rectangle r && r.side1() == r.side2() -> "Square: " + r;
case Rectangle r -> "Rectangle: " + r;
});
}
通过 when 子句在模式中附加布尔条件(Java 17 预览版中保护模式的关键字是 when):
String evaluate(Shape shape) {
return switch (shape) {
case Circle c when c.radius > 10 -> "大圆形";
case Circle c -> "小圆形";
case Rectangle r when r.w > r.h * 2 -> "宽矩形";
case Rectangle r -> "普通矩形";
};
}
switch 表达式 + yield
当分支需要多条语句时,用 {} 包裹并用 yield 返回值:
String classify(Object obj) {
return switch (obj) {
case String s -> {
System.out.println("处理字符串");
yield "字符串: " + s.toUpperCase();
}
case Integer i -> {
System.out.println("处理整数");
yield "整数: " + (i * 2);
}
default -> "未知";
};
}
穷尽性检查(与密封类协同)
当 switch 表达式的选择器是密封类或枚举时,编译器知道所有可能的子类型,强制要求覆盖全部情况,漏掉任何一个子类都会编译报错。
注意事项:
- 子类型优先:case 分支中子类型必须出现在父类型之前,否则编译报错。
- 不可混用语法:同一个 switch 块内不能混用 -> 和 : 两种风格。
- 模式变量作用域:绑定变量仅在对应分支的箭头右侧或冒号后的语句组内有效。
3. 恢复严格的浮点语义
其核心是让所有浮点运算在任何平台上都保证结果完全一致,并因此让 strictfp 关键字变得不再必要。默认将所有浮点运算视为严格模式,等价于每个类/方法都隐式加了 strictfp,从而消除不同平台、不同 JVM 实现之间浮点中间精度的差异。
Java 1.2 之前,浮点运算默认就是严格的;
但从 Java 1.2 开始,JVM 被允许在中间计算时使用扩展精度,例如 x86 架构的 x87 浮点协处理器会用 80 位扩展精度暂存中间结果。这带来一个问题:同一份代码在不同 CPU、不同操作系统、不同 JVM 上,可能得到略微不同的浮点结果。 对于科学计算、金融对账、分布式仿真、自动化测试等场景,这种微小差异会非常麻烦。
所以 Java 1.2 引入了 strictfp 关键字,强制浮点运算严格遵循 IEEE 754 标准:中间结果必须截断到 32 位 float 或 64 位 double,禁止使用硬件扩展精度。
Java 17 起,所有浮点运算默认都是严格的;不再区分“默认宽松语义”和“strictfp 严格语义”;strictfp 关键字仍然合法,但已无实际效果,编译器会忽略它;这相当于把 Java 1.2 之前的默认行为恢复回来。
✅ 示例1:默认严格语义
public class StrictDemo {
public static double compute(double a, double b, double c) {
// Java 17 中,这段代码默认就是严格浮点语义
return a * b + c;
}
public static void main(String[] args) {
// compute 方法内的浮点中间结果会严格遵循 IEEE 754 双精度语义,不会因平台不同而出现额外精度扩展。
double result = compute(0.1, 0.2, 0.3);
System.out.println(result);
}
}
✅ 示例2:strictfp 关键字不再必要
// Java 17 之前:需要显式声明 strictfp 保证跨平台一致
strictfp class OldStrictCalculator {
double calculate(double x, double y) {
return x * x / y;
}
}
// Java 17 之后:加不加 strictfp 效果相同
class NewCalculator {
double calculate(double x, double y) {
return x * x / y;
}
}
✅ 示例3:Math 与 StrictMath 行为更接近
double a = Math.sin(1.23456789);
double b = StrictMath.sin(1.23456789);
System.out.println(a == b); // 在严格浮点语义下更可能一致
用“始终严格”取代了“严格/默认”两种模式并存的状态,让 strictfp 关键字退出历史舞台。其核心价值在于:浮点运算的结果在任何平台上都完全一致,消除了因硬件架构差异导致的微妙数值偏差,对数值敏感型库(如 java.lang.Math、java.lang.StrictMath)的开发和跨平台协作尤为有利。
二、新增API/包
1. 增强的伪随机数生成器
Java 17 之前,Java 提供了 Random、ThreadLocalRandom 和 SplittableRandom 等随机数生成器,但它们各自独立,缺乏统一的接口,导致算法切换困难,且存在大量重复代码。
Java 17 新的 RandomGenerator 接口是所有随机数生成器的根接口。提供统一接口、工厂类和新算法,方便在不同算法之间切换,同时,统一 nextInt、nextLong、nextDouble、nextBoolean、流式方法等。
在此基础上,又派生出几个功能更专一的子接口:
SplittableGenerator:可以从当前生成器派生出新的生成器,适用于 fork/join 并行计算场景。JumpableGenerator:允许跳过中等数量的随机数抽取。LeapableGenerator:允许跳过大量的随机数抽取。ArbitrarilyJumpableGenerator:支持指定任意跳跃距离。
Java 17 同时引入了多个新的随机数算法实现,主要分为 Xoroshiro/Xoshiro 系列和 LXM 系列。
新的 RandomGeneratorFactory 是创建和管理随机数生成器的核心入口。你可以通过它列出所有可用的算法,并根据属性(如状态位数、是否可跳跃等)来筛选。
// 1. 使用默认高性能生成器
RandomGenerator defaultRng = RandomGenerator.getDefault();
System.out.println(defaultRng.nextInt(0, 100));
// 2. 通过工厂指定算法
RandomGeneratorFactory<RandomGenerator> factory =
RandomGeneratorFactory.of("L128X256MixRandom");
RandomGenerator rng = factory.create(12345L); // 指定种子
System.out.println(rng.nextInt(0, 100));
System.out.println(rng.nextDouble());
System.out.println(rng.nextBoolean());
// 3. 流式生成
rng.ints(5, 0, 10).forEach(System.out::println);
可拆分与可跳跃:
// 可拆分:适合并行任务
var splittable = new java.util.SplittableRandom();
var sub1 = splittable.split();
var sub2 = splittable.split();
// 可跳跃:跳过一段随机序列
var factory = RandomGeneratorFactory.of("Xoshiro256PlusPlus");
var jumpable = factory.create();
jumpable.jump(); // 向前跳跃一段
使用场景:
- 普通业务随机数、模拟、游戏逻辑:可以用 RandomGenerator 及其新算法。
- 高并发场景:LXM 系列通常比传统 Random 更合适。
- 并行计算:优先考虑 split() 派生独立子生成器。
- 安全场景:仍然必须使用 SecureRandom 或 RandomGenerator.Secure,不能用普通 PRNG。
2. 上下文特定的反序列化过滤器
反序列化不受信任的数据是 Java 长期面临的安全风险,攻击者可能构造恶意对象图来执行任意代码。
Java 17 的核心改进是引入了可配置的 JVM 全局过滤器工厂(Filter Factory),过滤器可以按线程、模块、调用栈、库、类加载器等上下文动态选择。让应用能够在每次反序列化操作时动态地、根据上下文选择或组合过滤器,从而降低反序列化漏洞风险。
核心API:
// 设置 JVM 范围的过滤器工厂
ObjectInputFilter.Config.setSerialFilterFactory(filterFactory);
// 获取当前过滤器工厂
ObjectInputFilter.Config.getSerialFilterFactory();
过滤器工厂是一个 BinaryOperator<ObjectInputFilter>,它会在每次创建 ObjectInputStream 以及调用其 setObjectInputFilter 方法时被调用。它接收两个参数:
- curr:当前流上已有的过滤器;
- next:新请求设置的过滤器;
当 ObjectInputStream 构造函数被调用时,工厂接收到的第一个参数(curr)是 null,第二个参数(next)是静态的 JVM 全局过滤器。当 setObjectInputFilter 被调用时,工厂会再次被调用,此时 curr 是之前返回的过滤器,next 是本次请求设置的新过滤器。
// 1. 定义一个自定义过滤器:只允许 com.example 包下的类
ObjectInputFilter customFilter =
ObjectInputFilter.Config.createFilter("com.example.*;!*");
// 2. 实现一个简单的过滤器工厂
BinaryOperator<ObjectInputFilter> factory = (curr, next) -> {
// 简单地将自定义过滤器与传入的 next 过滤器合并
// 实际应用中可根据线程或调用栈动态选择
if (next == null) {
return customFilter;
}
// 使用 merge 将两个过滤器组合,任一拒绝则整体拒绝
return ObjectInputFilter.merge(customFilter, next);
};
// 3. 设置 JVM 全局过滤器工厂(只能设置一次)
ObjectInputFilter.Config.setSerialFilterFactory(factory);
// 4. 后续任何 ObjectInputStream 的创建都将经过此工厂
// ... 进行反序列化操作 ...
三、工具与虚拟机更新
1. JDK 内部强封装
JDK 17 默认对所有 JDK 内部元素执行强封装,并移除了 --illegal-access 选项。除了 sun.misc.Unsafe 等极少数关键内部 API 外,其他内部类、内部包默认都不能被外部代码访问。
import java.lang.reflect.Field;
public class UnsafeReflectDemo {
public static void main(String[] args) throws Exception {
// 尝试反射访问 JDK 内部类
Class<?> clazz = Class.forName("jdk.internal.misc.Unsafe");
Field field = clazz.getDeclaredField("theUnsafe");
field.setAccessible(true); // JDK 17 默认会失败
Object unsafe = field.get(null);
System.out.println(unsafe);
}
}
// 抛出 InaccessibleObjectException 异常
// java.lang.reflect.InaccessibleObjectException:
Unable to make field accessible: module jdk.unsupported does not "opens jdk.internal.misc" to unnamed module
2. macOS/AArch64 移植
JDK 17 正式支持 macOS 上的 AArch64 架构,也就是 Apple Silicon,例如:M1、M2 系列芯片。主要变化:
- 提供原生 macos-aarch64 JDK 二进制包;
- 不再必须依赖 Rosetta 2 转译运行 x86_64 JDK;
- 支持 W^X 内存保护,即“写”和“执行”不能同时生效;实现方式是利用 Apple 提供的
pthread_jit_write_protect_np系统调用来动态切换内存的写/执行权限 - 既可以在 Intel Mac 上交叉编译,也可以在 Apple Silicon Mac 上直接编译。
3. 新的 macOS 渲染管线
这项特性为 macOS 上的 Java 2D 图形渲染引入了基于 Apple Metal API 的新管线,以替代已被 Apple 废弃的 OpenGL。
Apple 在 macOS 10.14 中弃用了 OpenGL,并推荐使用 Metal。Java 2D API 提供一个使用 Metal 框架的、功能完整的渲染管线,以确保 Java 图形应用能在未来的 macOS 版本中继续正常运行,并可能获得更好的性能。
在 JDK 17 中,Metal 管线是可选开启的,OpenGL 仍是默认选项。你可以通过在启动命令中指定系统属性来启用它:
java -Dsun.java2d.metal=true -jar your-swing-app.jar
4. 移除与弃用
- 移除 Applet API;
- 移除 RMI Activation;
- 移除实验性的 AOT 和 JIT 编译器;
- 弃用安全管理器(Security Manager)。