JDK 17 有哪些新特性?最全解析在这里 – wiki基地


JDK 17:Java 迈向新时代的里程碑——最全解析在这里

作为 Java 开发者,我们深知每一次 JDK 版本的发布都带来新的惊喜和挑战。而在众多版本中,LTS(长期支持)版本无疑具有举足轻重的地位。它们不仅带来了稳定、成熟的新特性,更是企业级应用选择升级时的首选。JDK 17,于 2021 年 9 月发布,正是这样一个重要的 LTS 版本,接替了 JDK 11 的位置,将为开发者提供长达数年的官方支持和安全更新。

JDK 17 的发布是 Java 发展史上的一个重要节点。它集成了自 JDK 12 到 JDK 16 之间的众多孵化、预览和最终确定(Final)的特性,涵盖了语言本身、JVM、核心库、垃圾回收、安全性以及工具等多个方面。理解并掌握 JDK 17 的新特性,对于提升开发效率、改善应用性能、增强安全性以及规划未来技术路线都至关重要。

本文将对 JDK 17 中的主要新特性进行深度剖析,带你全面了解这个新一代 LTS 版本带来的改变。我们将从语言特性、API 更新、JVM 改进、安全性增强以及工具升级等方面,逐一揭示 JDK 17 的魅力。

一、语言特性:让代码更具表现力和安全性

JDK 17 带来了一些重要的语言特性,其中最受瞩目的是 Sealed Classes 的最终确定和 Pattern Matching for switch 的预览。

1. Sealed Classes (JEP 409) – 最终确定

密封类(Sealed Classes)允许你精确地控制哪些类或接口可以扩展或实现一个密封类或接口。这个特性在 JDK 15 和 JDK 16 中经历了预览,终于在 JDK 17 中正式定稿(Final)。

解决了什么问题?

在 Java 中,继承和实现提供了强大的代码复用和多态能力。但有时候,你可能希望限制这种能力,只允许特定的几个类来继承或实现某个类或接口。传统的做法是使用默认(包私有)修饰符来限制跨包继承,或者依赖文档说明,但这都无法在编译器层面强制执行,容易出错。

工作原理:

通过在类或接口声明中使用 sealed 关键字,并在后面加上 permits 子句,明确列出允许直接扩展(对于类)或实现(对于接口)的子类或实现类。

“`java
// 声明一个密封接口 Shape,只允许 Circle 和 Square 来实现它
public sealed interface Shape permits Circle, Square {
double area();
}

// Circle 必须声明自己是 final, sealed, 或 non-sealed
// final: 表示 Circle 不允许被继承
public final class Circle implements Shape {
private double radius;
public Circle(double radius) { this.radius = radius; }
public double area() { return Math.PI * radius * radius; }
}

// Square 声明自己是 non-sealed,意味着 Square 可以被任意类继承
public non-sealed class Square implements Shape {
private double side;
public Square(double side) { this.side = side; }
public double area() { return side * side; }
}

// 如果你尝试让一个未在 permits 列表中声明的类实现 Shape,编译器会报错
// public class Triangle implements Shape { … } // Error: Triangle is not permitted to extend sealed interface Shape
“`

允许的继承/实现类型:

Sealed 类或接口的直接子类或实现类必须显式地声明自己是以下三种类型之一:
* final: 表示该类不能被进一步继承。
* sealed: 表示该类也是一个密封类,同样需要使用 permits 指定允许继承它的子类。
* non-sealed: 表示该类不再是密封的,可以被任意类继承。

优点:

  • 更强的可读性和设计意图表达: 代码清晰地表明了继承或实现的意图和范围。
  • 编译器强制检查: 任何未经许可的继承或实现都会导致编译错误,提高了代码的健壮性。
  • 支持未来的语言特性: Sealed Classes 是 Pattern Matching for switch 等特性进一步优化的基础,编译器和工具可以利用密封类的完备性信息进行更智能的分析。

Sealed Classes 是一个非常实用的特性,特别适用于定义有限集合的数据类型(如代数数据类型)或构建更加安全和可控的类库。

2. Pattern Matching for switch (JEP 406) – 第二次预览

这个特性旨在扩展 switch 语句和表达式的功能,使其不仅能匹配精确的值,还能根据对象的类型进行匹配,并自动进行类型转换,显著简化代码。在 JDK 17 中,它是第二次预览。

解决了什么问题?

传统的 switch 语句只能用于 byte, short, char, int, 其包装类, enum, String 以及从 JDK 14 开始支持的原始类型的包装类常量。对于更复杂的逻辑,例如根据对象的实际类型执行不同的操作,我们通常使用一系列的 if-else if 语句结合 instanceof 检查和类型转换,代码冗长且容易出错。

工作原理:

JEP 406 允许 switchcase 标签中使用类型模式(Type Pattern)。

“`java
Object obj = … ; // obj 可以是 String, Integer, Double, null 等

// 使用 if-else if 和 instanceof 的传统写法
if (obj instanceof String s) {
System.out.println(“这是字符串,长度为: ” + s.length());
} else if (obj instanceof Integer i) {
System.out.println(“这是整数,值为: ” + i);
} else if (obj instanceof Double d) {
System.out.println(“这是双精度浮点数,值为: ” + d);
} else if (obj == null) {
System.out.println(“这是 null”);
} else {
System.out.println(“未知类型”);
}

// 使用 Pattern Matching for switch (JDK 17 预览语法)
switch (obj) {
case String s -> System.out.println(“这是字符串,长度为: ” + s.length());
case Integer i -> System.out.println(“这是整数,值为: ” + i);
case Double d -> System.out.println(“这是双精度浮点数,值为: ” + d);
case null -> System.out.println(“这是 null”); // 可以直接处理 null case
default -> System.out.println(“未知类型”);
}
“`

改进点:

  • 类型匹配: case 标签后可以直接跟类型和变量名(如 String s),如果对象是该类型,则进入该分支,并且变量 s 在该分支的作用域内是已经类型转换好的。
  • Null Handling: switch 语句现在可以直接处理 null case,而不会抛出 NullPointerException(除非没有 null case 且输入为 null)。
  • Guard Clauses: 允许在类型模式后添加 when 子句来进一步细化匹配条件(例如 case String s when s.length() > 5 -> ...)。
  • Exhaustiveness: 对于密封类(Sealed Classes)或枚举,编译器可以检查 switch 语句是否覆盖了所有可能的子类型或枚举值,如果不全,可能会给出警告或错误(取决于是否作为表达式使用,表达式必须穷尽所有可能)。

优点:

  • 代码更简洁易读: 显著减少了 instanceof 和强制类型转换的代码。
  • 提高了安全性: 编译器辅助进行类型检查和转换,减少人为错误。
  • 增强了表达力: 更直观地表达了基于类型的分支逻辑。

需要注意的是,在 JDK 17 中使用此特性需要启用预览功能(编译和运行时都需添加 --enable-preview 参数)。它在后续版本中继续演进并最终确定。

二、核心库与 API 更新:提升功能与安全性

JDK 17 在核心库层面也带来了多项重要的改进和新 API。

1. Enhanced Pseudo-Random Number Generators (JEP 356)

增强的伪随机数生成器(PRNGs)API 提供了一组新的接口和实现,旨在更好地支持各种伪随机数算法,并提供更灵活的使用方式。

解决了什么问题?

Java 现有的 java.util.Random 类功能有限,不支持多种现代 PRNG 算法,且难以扩展和互换。

工作原理:

引入了一个新的接口家族,以 java.util.random.RandomGenerator 为核心。不同的算法(如 Xoshiro256++, LXM)实现了这个接口。

“`java
import java.util.random.RandomGenerator;
import java.util.random.RandomGeneratorFactory;

// 获取一个特定的随机数生成器实例
RandomGenerator generator = RandomGeneratorFactory.of(“LXM”).create();

// 使用新的方法生成随机数
int randomInt = generator.nextInt(100); // 生成 [0, 99] 的随机整数

// 遍历可用的随机数生成器工厂
RandomGeneratorFactory.all().map(Factory::name).sorted().forEach(System.out::println);
“`

优点:

  • 多样化的算法支持: 轻松使用和切换不同的高质量 PRNG 算法。
  • 更高的灵活性: 标准化的接口使得不同实现可以互换。
  • 更好的可测试性: 可以更容易地注入特定的 PRNG 实现进行测试。
  • 改进的性能和统计特性: 新增的算法通常比旧的 java.util.Random 算法在性能或统计特性上有所提升。

2. Foreign Function & Memory API (JEP 412) – 第二次孵化

这个 API 旨在提供一种安全、高效、便捷的方式,让 Java 程序能够调用本地代码(例如 C/C++ 库)和处理本地内存,而无需使用传统的 JNI(Java Native Interface)。在 JDK 17 中,它是第二次孵化(Incubator)状态。

解决了什么问题?

JNI 是 Java 与本地代码交互的标准方式,但它复杂、危险(容易导致 JVM 崩溃)且维护成本高。处理本地内存也缺乏安全且方便的机制。

工作原理(核心概念):

  • MemorySegment: 代表一段连续的本地内存区域。
  • MemoryAddress: 代表本地内存中的一个地址。
  • MemoryLayout: 描述本地内存的结构和布局。
  • Linker: 用于查找本地函数并生成对应的 Java 调用句柄。
  • FunctionDescriptor: 描述本地函数的签名(参数类型和返回类型)。

通过这个 API,开发者可以在 Java 代码中定义如何调用本地函数,并以结构化的方式访问本地内存,而这一切都在 JVM 的管理下进行,提供了更高的安全性和可移植性。

“`java
// 概念代码示例(实际使用更复杂)
import jdk.incubator.foreign.*; // 注意:这是孵化模块

// 假设有一个本地 C 函数 int add(int a, int b);
// 1. 找到本地库和函数
Path libPath = Path.of(“path/to/mylib.so”); // 或 .dll, .dylib
System.load(libPath.toString());
SymbolLookup loader = SymbolLookup.loaderLookup();
MethodHandle addHandle = CLinker.getInstance().downcallHandle(
loader.lookup(“add”).get(),
FunctionDescriptor.of(C_INT, C_INT, C_INT) // 描述函数签名:返回 int, 参数 int, int
);

// 2. 调用本地函数
int result = (int) addHandle.invokeExact(10, 20); // 结果是 30
System.out.println(“本地函数调用结果: ” + result);

// 3. 处理本地内存 (示例: 分配一块本地内存并写入数据)
try (MemorySegment segment = MemorySegment.allocateNative(100, ResourceScope.auto())) {
segment.set(C_INT, 0, 42); // 在地址 0 写入整数 42
int value = segment.get(C_INT, 0);
System.out.println(“从本地内存读取值: ” + value);
} // resource scope ensures memory is freed automatically
“`

优点:

  • 安全性更高: 在 JVM 控制下访问本地内存,减少了缓冲区溢出等风险。
  • 性能接近 JNI: 设计目标是提供与 JNI 相媲美的性能。
  • 易用性提升: 相较于 JNI,提供了更现代、更类型安全的 API。
  • 潜力巨大: 为高性能计算、机器学习库的绑定等领域打开了新的大门。

作为孵化模块,该 API 在后续版本中可能会有 API 变更,但在 JDK 17 中已经可以用于实验性项目。

3. Vector API (JEP 414) – Second Incubator

向量 API 旨在允许开发者利用支持 SIMD(Single Instruction, Multiple Data)指令集的 CPU 硬件能力来执行向量计算,从而显著提高某些计算密集型操作(如数值计算、图像处理)的性能。在 JDK 17 中,它是第二次孵化状态。

解决了什么问题?

Java 现有的代码在进行数组操作时,通常是按元素逐个处理,这无法有效利用现代 CPU 的向量指令(一次处理多个数据)。虽然 JIT 编译器可以尝试进行自动向量化,但这通常依赖于代码模式,且不够稳定和可控。

工作原理:

提供了一套 API 来表示向量(一组相同类型的基本数据),并定义了各种向量操作(加、减、乘、除、按位逻辑、比较、加载、存储等)。这些操作会映射到 CPU 的 SIMD 指令。

“`java
// 概念代码示例(实际使用更复杂)
import jdk.incubator.vector.*; // 注意:这是孵化模块

// 定义一个向量类型,例如处理 float 类型的向量
FloatVector vs = FloatVector.SPECIES_MAX; // 获取当前平台支持的最大 float 向量规范

// 假设有两个 float 数组 a 和 b,长度为 N
float[] a = …;
float[] b = …;
float[] c = new float[a.length];

// 循环按向量处理数组(伪代码)
for (int i = 0; i < a.length; i += vs.length()) {
// 从数组加载向量
FloatVector va = FloatVector.fromArray(vs, a, i);
FloatVector vb = FloatVector.fromArray(vs, b, i);

// 执行向量加法
FloatVector vc = va.add(vb);

// 将结果向量存回数组
vc.intoArray(c, i);

}
“`

优点:

  • 显著的性能提升: 对于适合向量化的任务,性能可以提高数倍甚至数十倍。
  • 跨平台: API 会根据底层硬件自动选择最优的 SIMD 指令集(如 SSE、AVX、NEON)。
  • 可控性: 开发者可以显式地使用向量 API 来指导 JIT 编译器生成向量指令。

与 Foreign Function & Memory API 类似,Vector API 也是一个仍在发展中的孵化模块,但其潜力巨大,对于需要极致计算性能的应用场景非常有价值。

4. Context-Specific Deserialization Filters (JEP 415)

此 JEP 扩展了反序列化过滤机制,允许在每个 ObjectInputStream 流级别配置反序列化过滤器。

解决了什么问题?

反序列化是 Java 中一个已知的安全风险点(如著名的 Apache Commons Collections 反序列化漏洞),攻击者可以通过精心构造的序列化数据来执行恶意代码。虽然 JDK 9 引入了全局的反序列化过滤器(通过系统属性 jdk.serialFilter 配置),但这不够灵活,无法针对不同的场景应用不同的过滤策略。

工作原理:

在创建 ObjectInputStream 时,可以通过新的 setObjectInputFilter 方法为其设置一个特定的过滤器。这个过滤器会覆盖全局过滤器(如果设置了的话),或者在没有全局过滤器时单独生效。

“`java
// 示例:只允许反序列化 String 和 Integer
ObjectInputFilter myFilter = ObjectInputFilter.Config.createFilter(
“java.lang.String;java.lang.Integer;!*” // 允许 String, Integer, 拒绝所有其他
);

try (FileInputStream fis = new FileInputStream(“data.ser”);
ObjectInputStream ois = new ObjectInputStream(fis)) {

// 在流级别设置过滤器
ois.setObjectInputFilter(myFilter);

// 反序列化对象
Object obj1 = ois.readObject();
Object obj2 = ois.readObject();
// ...

} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
“`

优点:

  • 提高了安全性: 允许应用程序针对不同的数据源或用例应用更精细、更严格的反序列化过滤策略。
  • 增强了灵活性: 过滤规则可以在运行时动态配置,无需修改全局系统属性。

这是一个重要的安全增强,建议所有处理不受信任的反序列化数据的应用都考虑使用此特性。

三、JVM 改进与性能优化

除了语言特性和 API,JDK 17 也带来了 JVM 本身的改进,特别是在垃圾回收和对新硬件的支持方面。

1. Restore Always-Strict Floating-Point Semantics (JEP 306)

此 JEP 将浮点运算的严格模式 (strictfp) 恢复为默认行为。

解决了什么问题?

历史上,Java 的浮点运算默认是严格的 (strictfp),但在 JDK 1.2 中为了适应某些处理器的优化,引入了非严格模式,并需要使用 strictfp 关键字来强制执行严格模式。这导致浮点计算结果在不同平台或不同 JVM 版本上可能存在微小的差异,影响了可移植性。

工作原理:

在 JDK 17 中,所有的浮点运算都默认遵循严格的浮点语义。这意味着,无论是否使用 strictfp 关键字,计算结果都将是可预测和可移植的。

“`java
// 在 JDK 17 中,即使没有 strictfp 关键字,这里的计算结果也遵循严格语义
public class StrictFPDemo {
public double calculate() {
double x = 0.1;
double y = 0.2;
double z = (x + y) * 10; // 严格语义下,结果更精确
return z;
}

// strictfp 关键字仍然是合法的,但在这里是冗余的
public strictfp double calculateStrict() {
     double x = 0.1;
    double y = 0.2;
    double z = (x + y) * 10;
    return z;
}

}
“`

优点:

  • 增强了可移植性: 确保浮点计算在不同环境下产生相同的结果。
  • 简化了开发: 开发者无需再担心 strictfp 的使用或不同模式下的差异。

2. New macOS Rendering Pipeline (JEP 382) & macOS AArch64 Port (JEP 391)

这两个 JEP 共同为 Java 在 Apple Silicon(基于 AArch64 架构,如 M1、M2 芯片)上的运行提供了全面支持。

  • JEP 391 (macOS AArch64 Port): 提供了在 Apple Silicon 架构上原生运行 JDK 的能力。这意味着 Java 应用无需通过 Rosetta 2 转译即可获得最佳性能。
  • JEP 382 (New macOS Rendering Pipeline): 为 macOS 上的 Swing 和 AWT 应用程序引入了一个新的 Metal 渲染管道,替代了旧的 OpenGL 管道。这改善了 GUI 应用在 macOS 上的性能和与操作系统的集成度,尤其是在 Apple Silicon 上。

优点:

  • 提升在 Apple Silicon 上的性能: 原生运行和优化的渲染管道显著提高了 Java 应用在 M 系列芯片上的启动速度、响应性和图形性能。
  • 更好的用户体验: Swing/AWT 应用在 macOS 上看起来和感觉更像是原生的。

3. 其他 JVM 改进

JDK 17 还包含 G1 垃圾回收器的多项改进、ZGC 和 Shenandoah 等实验性 GC 的更新,以及 JIT 编译器(C2)的性能优化。这些改进通常是透明的,用户无需做任何修改即可受益于潜在的吞吐量或延迟改善。

四、安全性:移除旧特性与加强封装

安全性一直是 Java 的重点关注领域。JDK 17 通过移除过时或存在潜在风险的功能,并加强对内部 API 的封装,进一步提升了平台的安全性。

1. Strongly Encapsulate JDK Internals (JEP 403)

这个 JEP 强制对 JDK 的内部 API 进行强封装。自 JDK 9 引入模块化系统以来,就一直在逐步收紧对内部 API 的访问。JDK 17 是这一过程中的一个重要里程碑。

解决了什么问题?

许多应用程序和库长期以来依赖于 sun.*com.sun.*jdk.internal.* 等非公开的内部 API。这些 API 是不稳定的,可能在任何版本中更改或移除,使用它们会破坏应用程序的向前兼容性,并绕过模块系统的安全性。

工作原理:

在 JDK 17 中,除了少数例外(如 sun.misc.Unsafe 在特定条件下仍然可以访问),默认情况下,对 JDK 内部包的反射访问(包括深度反射)将被拒绝。

  • 在 JDK 16 及之前, --illegal-access=permit 是默认值,允许对内部 API 进行非法访问并发出警告。
  • 在 JDK 17 中, --illegal-access=deny 成为默认值。任何非法访问都会抛出 IllegalAccessException

影响:

如果你的应用程序或依赖库使用了 JDK 的内部 API 且未进行适配,在 JDK 17 上运行时很可能会出现 IllegalAccessException,导致程序崩溃。

应对方案:

  • 优先使用标准公共 API: 查找是否有标准的公共 API 替代方案。这是最推荐的做法。
  • 联系库维护者: 如果问题出在第三方库,请等待或升级到支持 JDK 17 的版本。
  • 使用 --add-opens 作为临时或不得已的方案,可以使用 --add-opens <module>/<package>=<target-module> JVM 参数来显式开放特定的内部包给特定的模块(通常是你的应用程序模块或 ALL-UNNAMED)。例如:--add-opens java.base/sun.nio.ch=ALL-UNNAMED。但这会削弱模块系统的保护,应谨慎使用。

JEP 403 是 Java 平台走向更加安全和可维护的重要一步,虽然可能给升级带来一些兼容性挑战,但长期来看对整个生态系统是有益的。

2. Deprecate the Security Manager for Removal (JEP 411)

安全管理器(Security Manager)是一种提供细粒度安全策略的旧机制,但它复杂、难以正确配置,并且在现代应用开发(尤其是在云原生环境)中已不再是主流的安全沙箱机制。

工作原理:

在 JDK 17 中,安全管理器被标记为已废弃(deprecated),并明确声明将在未来的版本中移除。调用 System.setSecurityManager 会触发警告。

影响:

依赖于 Security Manager 的应用程序(这在 applet、Web Start 或某些老旧的企业应用中比较常见)需要考虑迁移到其他安全机制。

优点:

  • 移除遗留复杂性: 简化了平台,减轻了维护负担。
  • 鼓励采用现代安全实践: 促使开发者转向更适合当前环境的安全模型。

3. Deprecate the Applet API for Removal (JEP 398)

Applet API 早已过时,现代浏览器早已停止对其的支持。

工作原理:

在 JDK 17 中,Applet API 被标记为已废弃,并明确声明将在未来的版本中移除。

优点:

  • 清理遗留 API: 移除不再使用的代码,简化 JDK。

五、移除和废弃的功能:精简平台

JDK 17 还移除了一些在之前版本中已废弃的功能,进一步精简了平台。

1. Remove RMI Activation (JEP 407)

RMI Activation 是 Java RMI(Remote Method Invocation)的一个组件,用于按需启动 RMI 对象。由于其复杂性和较低的使用率,已经被移除。

2. Remove the Experimental AOT and JIT Compiler (JEP 410)

实验性的提前编译(AOT)和实验性的 JIT 编译器配置(--force-gc-z)被移除。这些特性在实验阶段未能获得广泛采用或达到预期效果。

六、总结:为何选择 JDK 17?

JDK 17 作为一个 LTS 版本,带来了稳定性、性能提升、安全加固以及一系列令人兴奋的新特性。对于企业和开发者而言,升级到 JDK 17 具有以下显著优势:

  1. 长期支持与安全性: LTS 版本意味着可以获得多年的官方安全更新和 bug 修复,这是生产环境应用的关键。
  2. 性能提升: JVM 本身的优化,特别是在 Apple Silicon 上的原生支持和渲染改进,以及未来 Vector API 和 FFM API 的潜力,都能带来性能上的收益。
  3. 安全性增强: 强制封装 JDK 内部 API、废弃 Security Manager 和加强反序列化过滤等措施,提高了平台的整体安全性。
  4. 语言特性提升: Sealed Classes 的最终确定提供了更强大的类型建模能力,而 Pattern Matching for switch 的预览则极大地简化了基于类型的分支逻辑,提升了开发效率和代码质量。
  5. 清理遗留代码: 移除过时或不安全的功能,使得平台更加现代化和易于维护。

当然,升级到新的 LTS 版本也可能带来一些兼容性挑战,特别是由于 JEP 403 对内部 API 的强制封装。但这通常可以通过更新依赖库或进行适度的代码调整来解决,并且带来的长期收益是巨大的。

总而言之,JDK 17 是 Java 平台发展中的一个重要里程碑,它巩固了 Java 在现代软件开发领域的地位,并为未来的创新奠定了坚实的基础。无论是为了安全性、性能还是为了利用最新的语言特性,升级到 JDK 17 都是一个值得认真考虑和规划的步骤。

希望这篇文章为你全面解析了 JDK 17 的新特性,帮助你更好地理解和应用这个重要的 LTS 版本!


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部