2.7 Java 17 版本新增特性和类

0 阅读12分钟

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,不能组合使用;
    1. final:不能再被继承
    2. sealed:继续限制继承(需要自己的 permits 子句)
    3. 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)。

参考文献:

  1. www.oracle.com/java/techno…
  2. openjdk.org/projects/jd…