JDK 24 新特性深度解析与代码示例
一、引言 (Introduction)
Java Development Kit (JDK) 24 于 2025 年 3 月 18 日正式发布 [1],带来了 Java SE 平台和 JDK 的参考实现。此版本包含了多达 24 个 JDK 增强提案 (JEPs),是自基于时间的发布计划引入以来新特性数量最多的版本之一 [2]。这些 JEPs 涵盖了语言改进、API 增强、性能提升、安全更新以及工具优化等多个方面,旨在进一步提升开发者生产力、平台性能和安全性。本报告将对 JDK 24 中的关键新特性进行详细总结,并尽可能提供代码示例以帮助理解。
二、JDK 24 主要特性概览 (Overview of JDK 24 Main Features)
JDK 24 引入了众多 JEPs,其中一些是预览特性,意味着它们的设计和实现已完成,但尚未永久化,可能会在未来的版本中发生变化或被移除;另一些则是正式特性或实验性特性。下表总结了 JDK 24 中包含的主要 JEPs 及其状态:
JEP 编号 | 标题 | 类型与状态 | 核心领域 |
---|---|---|---|
JEP 488 | 模式、instanceof 和 switch 中的基本类型 (第二次预览) | 语言预览 | 语言特性 |
JEP 492 | 灵活的构造函数体 (第三次预览) | 语言预览 | 语言特性 |
JEP 494 | 模块导入声明 (第二次预览) | 语言预览 | 语言特性 |
JEP 495 | 简单源文件和实例主方法 (第四次预览) | 语言预览 | 语言特性 |
JEP 487 | 作用域值 (第四次预览) | API 预览 | 核心库/并发 |
JEP 485 | Stream Gatherers | 正式特性 | 核心库/Stream API |
JEP 484 | Class-File API | 正式特性 | JVM/工具 |
JEP 450 | 紧凑对象头 (实验性) | JVM 实验性 | 性能/运行时 |
JEP 475 | G1 的晚期屏障扩展 | JVM | 性能/运行时 |
JEP 483 | Ahead-of-Time 类加载与链接 | JVM | 性能/运行时 |
JEP 491 | 无需固定的虚拟线程同步 | JVM/并发 | 性能/运行时 |
JEP 493 | 无需 JMOD 的运行时镜像链接 | 工具 | 工具 |
JEP 486 | 永久禁用安全管理器 | 安全性 | 安全性 |
JEP 496 | 抗量子模块化格密码密钥封装机制 | 安全性 | 安全性 |
JEP 497 | 抗量子模块化格密码数字签名算法 | 安全性 | 安全性 |
JEP 498 | 警告使用 sun.misc.Unsafe 中的内存访问方法 | JVM/安全性 | 安全性/迁移 |
JEP 489 | Vector API (第九次孵化) | API 孵化 | 核心库/性能 |
JEP 404 | 分代 Shenandoah (实验性) | JVM 实验性 | GC/性能 |
JEP 472 | 准备限制 JNI 的使用 | JVM/安全性 | 安全性/迁移 |
JEP 478 | 密钥派生函数 API (预览) | API 预览 | 安全性 |
JEP 479 | 移除 Windows 32位 x86 移植 | 平台 | 平台支持 |
JEP 490 | ZGC: 移除非分代模式 | JVM/GC | GC/性能 |
数据来源: [1]
这些 JEPs 共同构成了 JDK 24 的核心内容,为 Java 生态系统带来了显著的进步。
三、语言特性预览 (Language Feature Previews)
JDK 24 继续推进多个语言特性的预览,收集反馈并逐步完善。
1. JEP 488: 模式、instanceof 和 switch 中的基本类型 (第二次预览)
此 JEP 旨在增强模式匹配,允许在所有模式上下文中使用基本类型,并将 instanceof 和 switch 扩展为支持所有基本类型 [4]。这将使得对基本类型的数据探索更加统一和富有表现力。
目标与动机:
- 统一数据探索:允许对所有类型(无论是基本类型还是引用类型)使用类型模式。
- 对齐 instanceof 与安全转换:扩展 instanceof 以支持基本类型,使其能够作为安全转换的前提条件。
- 提升 switch 的表达能力:允许 switch 处理所有基本类型的值,例如 boolean、float、double 和 long [6]。
主要变更及代码示例:
-
instanceof 支持基本类型模式: 以前,instanceof 模式匹配仅支持引用类型。现在,它可以检查值是否可以安全地转换为给定的基本类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14// JEP 488 之前,需要手动进行范围检查
int i = 1000;
if (i >= -128 && i <= 127) {
byte b = (byte)i;
//... 使用 b...
}
// 使用 JEP 488
if (i instanceof byte b) {
//... 使用 b,如果匹配,则转换无信息损失...
System.out.println("Value fits in byte: " + b);
} else {
System.out.println("Value does not fit in byte.");
}对于
int i = 1000;
,输出将是 “Value does not fit in byte.”。如果int i = 100;
,则输出 “Value fits in byte: 100” [6]。 -
switch 支持所有基本类型: switch 语句和表达式现在可以处理 boolean、long、float 和 double 类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// boolean switch
boolean isLoggedIn = true;
String accessLevel = switch (isLoggedIn) {
case true -> "User";
case false -> "Guest";
};
System.out.println("Access Level: " + accessLevel); // 输出: Access Level: User
// float switch
float value = 10.5f;
switch (value) {
case 0.0f -> System.out.println("Zero");
case 10.5f -> System.out.println("Ten point five");
case float f -> System.out.println("Other float: " + f); // 基本类型模式
}
// 输出: Ten point five注意,case 常量必须与选择器表达式的类型相同(或其对应的包装类型),以防止有损转换。使用两个在表示上等效的浮点字面量作为 case 标签会导致编译时错误 [6]。
-
基本类型在记录模式中: 允许在记录模式中对基本类型组件进行更灵活的匹配,包括安全的窄化转换。
1
2
3
4
5
6
7
8
9
10
11
12
13record Point(double x, double y) {}
static void printIntCoordinates(Object obj) {
if (obj instanceof Point(int ix, int iy)) { // 使用 int 模式匹配 double 组件
System.out.println("Integer coordinates: (" + ix + ", " + iy + ")");
} else if (obj instanceof Point(double dx, double dy)) {
System.out.println("Double coordinates: (" + dx + ", " + dy + ")");
}
}
// 调用
printIntCoordinates(new Point(10.0, 20.0)); // 输出: Integer coordinates: (10, 20)
printIntCoordinates(new Point(10.5, 20.3)); // 输出: Double coordinates: (10.5, 20.3)在此示例中,如果 Point 的 double 组件可以无损转换为 int,则
Point(int ix, int iy)
模式会匹配 [6]。
此 JEP 的第二次预览旨在收集更多反馈,以期未来将其最终确定。
2. JEP 492: 灵活的构造函数体 (第三次预览)
JEP 492 提议允许在 Java 构造函数中,在显式构造函数调用(即 super(...)
或 this(...)
)之前出现语句 [3]。这些语句不能引用正在构造的实例,但可以初始化其字段或执行其他准备工作。
目标与动机:
- 更自然地放置构造函数参数的验证或转换逻辑,避免将其拆分到静态辅助方法或辅助构造函数中 [8]。
- 提高代码可读性和可维护性。
- 在方法被重写时,通过在调用超类构造函数之前初始化字段,可以使类更加可靠 [4]。
主要变更及代码示例: 构造函数体现在被分为两个阶段:super(...)
或 this(...)
调用之前的“序言”(prologue)和之后的“尾声”(epilogue)。
-
序言中的语句:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31class SuperClass {
SuperClass(String arg) {
System.out.println("SuperClass constructor called with: " + arg);
}
}
class SubClass extends SuperClass {
private final int processedValue;
SubClass(String rawValue) {
// 序言开始
if (rawValue == null || rawValue.trim().isEmpty()) {
throw new IllegalArgumentException("Raw value cannot be null or empty");
}
String processedArg = "[" + rawValue.trim() + "]";
// 序言结束
super(processedArg); // 显式构造函数调用
// 尾声开始
this.processedValue = rawValue.length();
System.out.println("SubClass instance initialized. Processed value: " + this.processedValue);
// 尾声结束
}
}
// 调用
// new SubClass(" test ");
// 输出:
// SuperClass constructor called with: [test]
// SubClass instance initialized. Processed value: 8在上述示例中,对
rawValue
的验证和处理 (processedArg
) 发生在super(processedArg)
调用之前 [9]。 -
限制:
- 序言中的语句不能通过
this
引用当前正在构造的实例(例如调用实例方法或访问实例字段,除非是初始化当前类声明的字段)。 - 序言中不允许
return
语句。 - 可以读取和写入当前类声明的字段,前提是这些字段没有自己的初始化器。在显式构造函数调用之前,不能读取任何字段(无论是当前类还是超类的)。
- 序言中的语句不能通过
此 JEP 的第三次预览继续完善这一特性,目标是使构造函数的编写更加灵活和直观。
3. JEP 494: 模块导入声明 (第二次预览)
JEP 494 旨在通过允许简洁地导入模块导出的所有包来增强 Java 语言 [4]。这简化了模块化库的重用,且不要求导入代码本身位于模块中。
目标与动机:
- 简化模块化库的重用,允许一次性导入整个模块的 API [11]。
- 减少大量
import com.foo.bar.*;
声明的冗余。 - 帮助初学者更容易使用第三方库和核心 Java 类,而无需深入了解其包层次结构 [11]。
主要变更及代码示例: 模块导入声明的形式为 import module M;
。它会按需导入模块 M 导出到当前模块的所有公共顶层类和接口,以及由于读取模块 M 而导致当前模块读取的其他模块所导出的包 [11]。
-
基本用法: 假设有一个
com.example.mathutils
模块,它导出了com.example.mathutils.algebra
和com.example.mathutils.geometry
包。1
2
3
4
5
6
7
8
9
10
11
12
13
14// 传统方式
// import com.example.mathutils.algebra.*;
// import com.example.mathutils.geometry.*;
// 使用 JEP 494
import module com.example.mathutils;
public class Main {
public static void main(String[] args) {
//可以直接使用来自 algebra 和 geometry 包的类
//例如:var point = new Point2D(1,2);
//例如:var matrix = new Matrix(2,2);
}
}编译和运行时需要启用预览特性:
javac --release 24 --enable-preview Main.java
和java --enable-preview Main
[11]。 -
与
java.base
和java.se
的交互: 一个重要的变更是,解除了任何模块都不能声明对java.base
模块的传递依赖的限制。java.se
模块的声明已被修改为传递性地依赖java.base
模块。因此,import module java.se;
现在将按需导入整个 Java SE API,包括java.base
中的所有包 [11]。1
2
3
4
5
6
7
8
9
10
11
12
13
14// 之前可能需要:
// import java.util.*;
// import java.util.function.*;
// import java.util.stream.*;
//... 等其他 java.base 包
// 现在,如果代码在模块化环境中且该模块依赖 java.se:
import module java.se; // 导入整个标准 Java API
void main() {
List<String> list = new ArrayList<>();
Stream<String> stream = list.stream();
//...
} -
歧义解决: 如果不同的导入模块导出了同名单的类,编译器会报错。可以通过更具体的单类型导入声明或按需包导入声明来解决歧义,这些声明会“遮蔽”模块导入声明 [11]。
1
2
3
4
5
6
7
8
9
10
11
12import module java.desktop; // 导出 java.awt.List
import module java.base; // 导出 java.util.List
// import java.util.List; // 使用此声明来消除 List 的歧义,使其指向 java.util.List
public class AmbiguityExample {
public static void main(String[] args) {
// List list; // 如果没有上面的单类型导入,这里会产生歧义错误
java.util.List<String> utilList = new java.util.ArrayList<>(); // 使用全限定名
System.out.println(utilList);
}
}
此 JEP 的第二次预览旨在通过实际使用收集更多反馈,特别是在与 java.se
模块的交互以及导入声明的遮蔽规则方面。
4. JEP 495: 简单源文件和实例主方法 (第四次预览)
JEP 495 致力于改进 Java 语言,使初学者能够编写他们的第一个程序,而无需理解为大型程序设计的语言特性 [4]。目标是简化单类程序的声明,并允许这些程序随着开发者技能的增长无缝扩展以使用更高级的特性。
目标与动机:
- 降低 Java 的入门门槛,减少编写简单程序(如 “Hello, World!”)所需的样板代码 [13]。
- 允许主方法 (
main
) 作为实例方法存在,无需static
修饰符。 - 引入“隐式声明的类”的概念,使得源文件可以直接包含方法和字段,而无需显式的
class
声明。 - 不引入 Java 语言的单独方言或单独的工具链 [13]。
主要变更及代码示例:
-
实例主方法:
main
方法不再强制要求是public static
。它可以是实例方法,甚至可以没有String[] args
参数。1
2
3
4
5
6// HelloWorld.java
class HelloWorld {
void main() { // 实例主方法,无参数
System.out.println("Hello, instance main!");
}
}使用源文件启动器运行:
java --enable-preview HelloWorld.java
[13]。 JVM 启动时:- 如果类包含带
String[]
参数的main
方法,则选择该方法。 - 否则,如果类包含无参数的
main
方法,则选择该方法。 - 如果选择的方法是
static
,则直接调用。 - 否则(实例 main 方法),类必须有一个无参数的非私有构造函数。启动器调用该构造函数创建对象,然后调用该对象的 main 方法 [13]。
- 如果类包含带
-
简单源文件 (隐式声明的类): 源文件可以直接包含方法和字段,编译器会隐式声明一个类。
1
2
3
4
5
6
7
8
9
10
11// Simple.java
String greeting = "Hello from simple source file!"; // 隐式类的字段
void main() { // 隐式类的主方法
System.out.println(greeting);
greet("World");
}
void greet(String name) { // 隐式类的辅助方法
System.out.println("Greetings, " + name + "!");
}运行:
java --enable-preview Simple.java
[13]。 隐式声明的类是final
的,位于未命名包中,名称通常派生自文件名。它必须有一个可启动的 main 方法 [13]。 -
自动导入
java.base
模块和java.io.IO
: 为了进一步简化,简单源文件会自动导入java.base
模块中的所有公共顶层类和接口(相当于import module java.base;
),以及java.io.IO
类中的五个静态方法 (println(Object)
,println()
,print(Object)
,readln(String)
,readln()
)(相当于import static java.io.IO.*;
)[13]。1
2
3
4
5
6
7
8
9// AutoImportDemo.java
void main() {
List<String> names = List.of("Alice", "Bob"); // List 来自 java.util (java.base)
for (String name : names) {
println("Name: " + name); // println 来自 java.io.IO
}
String input = readln("Enter something: "); // readln 来自 java.io.IO
println("You entered: " + input);
}运行:
java --enable-preview AutoImportDemo.java
[13]。
这是该特性的第四次预览,表明其正朝着最终确定的方向稳步发展,目标是显著改善 Java 的初学者体验。
5. JEP 487: 作用域值 (第四次预览)
JEP 487 引入了作用域值(Scoped Values),这是一种在线程内及其子线程间共享不可变数据的新机制 [5]。作用域值比线程局部变量(ThreadLocal)更容易推理,并且具有更低的空间和时间成本,尤其是在与虚拟线程(JEP 444)和结构化并发(JEP 480)一起使用时 [3]。
目标与动机:
- 易用性: 使数据流更易于理解。
- 可理解性: 共享数据的生命周期应从代码的句法结构中显而易见。
- 健壮性: 调用者共享的数据只能由合法的被调用者检索。
- 性能: 高效地在大量线程间共享数据,读取作用域值的成本通常与读取局部变量相当 [14]。
- 不可变性和有界生命周期: 与可变的 ThreadLocal 不同,作用域值一次写入后,仅在线程执行的特定有界时段内可用,避免了与 ThreadLocal 相关的内存泄漏风险 [14]。
主要变更及代码示例: ScopedValue
实例通常声明为 static final
。通过 ScopedValue.where(scopedValue, value).run(() -> {... })
或 .call(() -> {... })
来绑定值并在特定作用域内执行代码。
-
基本用法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24import static java.lang.ScopedValue.where; // 可以静态导入
public class ScopedValueExample {
private static final ScopedValue<String> USER_CONTEXT = ScopedValue.newInstance();
public static void main(String[] args) {
// 绑定 USER_CONTEXT 的值并执行操作
where(USER_CONTEXT, "UserA")
.run(() -> processRequest());
// 在此作用域之外,USER_CONTEXT.get() 会抛出异常
// System.out.println(USER_CONTEXT.get()); //会抛出 NoSuchElementException
}
static void processRequest() {
System.out.println("Processing request for: " + USER_CONTEXT.get()); // "UserA"
logActivity();
}
static void logActivity() {
// 即使在深层调用栈中,也能访问到绑定的值
System.out.println("Logging activity for: " + USER_CONTEXT.get()); // "UserA"
}
}[14]
-
嵌套作用域和重新绑定: 被调用者可以为同一个
ScopedValue
建立新的嵌套绑定,以向其自身的被调用者传递不同的值。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public class NestedScopedValueExample {
private static final ScopedValue<String> TRANSACTION_ID = ScopedValue.newInstance();
public static void main(String[] args) {
where(TRANSACTION_ID, "TXN-MAIN-001").run(() -> handleOuterTask());
}
static void handleOuterTask() {
System.out.println("Outer task, TXN ID: " + TRANSACTION_ID.get()); // TXN-MAIN-001
// 为内部任务重新绑定 TRANSACTION_ID
where(TRANSACTION_ID, "TXN-INNER-123").run(() -> handleInnerTask());
// 内部任务结束后,TRANSACTION_ID 恢复为外部任务的值
System.out.println("Back to outer task, TXN ID: " + TRANSACTION_ID.get()); // TXN-MAIN-001
}
static void handleInnerTask() {
System.out.println("Inner task, TXN ID: " + TRANSACTION_ID.get()); // TXN-INNER-123
}
}[14]
-
与结构化并发的集成: 通过
StructuredTaskScope
创建的子线程会自动继承父线程中的作用域值,开销极小。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 假设 USER_CONTEXT 已在父线程中通过 where().run() 绑定
public Response handle(Request request, Response response) {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Supplier<UserInfo> user = scope.fork(() -> {
此处可以访问 USER_CONTEXT.get()
return readUserInfo();
});
Supplier<List<Offer>> offers = scope.fork(() -> {
此处也可以访问 USER_CONTEXT.get()
return fetchOffers();
});
scope.join().throwIfFailed();
return new Response(user.get(), offers.get());
} catch (Exception ex) { /*... */ }
}[14]
JEP 487 的第四次预览主要移除了 ScopedValue
类中的 callWhere
和 runWhere
方法,使得 API 完全流畅,所有绑定操作都通过 ScopedValue.Carrier.call
和 ScopedValue.Carrier.run
(或静态导入的 where(...).call/run
) 进行 [14]。这表明该特性正趋于稳定。
四、正式特性 (Finalized Features)
JDK 24 将一些先前预览或孵化的特性最终确定为正式特性。
1. JEP 485: Stream Gatherers
JEP 485 在经过 JDK 22 (JEP 461) 和 JDK 23 (JEP 473) 的两轮预览后,正式引入了 Stream Gatherers [16]。它通过支持自定义中间操作来增强 Stream API,使得流管道能够以现有内置中间操作难以实现的方式转换数据 [5]。
目标与动机:
- 使流管道更加灵活和富有表现力。
- 允许自定义中间操作处理无限大小的流 [17]。
- 解决现有 Stream API 在表达某些复杂转换(如按自定义条件去重 distinctBy,或创建固定/滑动窗口 windowFixed/windowSliding)时的局限性 [17]。
核心概念与代码示例: Stream::gather(Gatherer)
是一个新的中间流操作,它应用一个用户定义的 Gatherer
实例来处理流元素。Gatherer
接口类似于 Collector
接口,但用于中间操作 [17]。 一个 Gatherer
由四个函数定义:
- initializer (可选): 提供一个在处理流元素时维护私有状态的对象。
- integrator: 集成输入流中的新元素,可能会检查私有状态对象,并可能向输出流发出元素。它可以提前终止处理。
- combiner (可选): 用于在输入流标记为并行时并行评估收集器。
- finisher (可选): 在没有更多输入元素时调用,可以检查私有状态并可能发出额外的输出元素。
[17]
-
内置 Gatherers:
java.util.stream.Gatherers
类提供了一些有用的预定义 gatherers,例如:-
windowFixed(int windowSize): 将流元素分组为固定大小的不重叠列表。
1
2
3
4
5
6
7
8
9import java.util.List;
import java.util.stream.Gatherers;
import java.util.stream.IntStream;
List<List<Integer>> fixedWindows = IntStream.range(0, 10).boxed()
.gather(Gatherers.windowFixed(3))
.toList();
// 输出: [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]
System.out.println("Fixed windows: " + fixedWindows);[18]
-
windowSliding(int windowSize): 创建流元素的滑动窗口列表。
1
2
3
4
5
6
7
8
9import java.util.List;
import java.util.stream.Gatherers;
import java.util.stream.IntStream;
List<List<Integer>> slidingWindows = IntStream.range(0, 5).boxed()
.gather(Gatherers.windowSliding(3))
.toList();
// 输出: [[0, 1, 2], [1, 2, 3], [2, 3, 4]]
System.out.println("Sliding windows: " + slidingWindows);[18]
-
fold(Supplier
initialValue, BiFunction<R,? super T, R> folder) : 执行左折叠操作,但只发出最终结果。1
2
3
4
5
6
7
8
9
10
11
12import java.util.Optional;
import java.util.function.Supplier;
import java.util.function.BiFunction;
import java.util.stream.Gatherers;
import java.util.stream.Stream;
// 计算数字流的总和
Optional<Integer> sum = Stream.of(1, 2, 3, 4, 5)
.gather(Gatherers.fold(() -> 0, (acc, val) -> acc + val))
.findFirst();
// 输出: Optional[15]
sum.ifPresent(s -> System.out.println("Fold sum: " + s));[18]
-
scan(Supplier
initialValue, BiFunction<R,? super T, R> scanner) : 类似于 fold,但发出所有中间累积结果。1
2
3
4
5
6
7
8
9
10
11import java.util.List;
import java.util.function.Supplier;
import java.util.function.BiFunction;
import java.util.stream.Gatherers;
import java.util.stream.Stream;
// 计算累积和
List<Integer> scannedSums = Stream.of(1, 2, 3, 4, 5)
.gather(Gatherers.scan(() -> 0, (acc, val) -> acc + val)) // 假设初始值参与到第一个结果中
.toList();
System.out.println("Scanned sums: " + scannedSums); // 预期: [1, 3, 6, 10, 15]说明:根据 JEP 485 和 javaalmanac 的例子 [18],
scan
通常包含初始值在结果中,或者行为略有不同。上面的代码是根据描述模拟的常见 scan 行为,即发出每个累积结果。
-
-
自定义 Gatherer 示例 (去重相邻元素):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34import java.util.List;
import java.util.stream.Gatherer;
import java.util.stream.Gatherers;
import java.util.stream.Stream;
import java.util.Objects;
public class DistinctAdjacent {
public static <T> Gatherer<T,?, T> distinctAdjacent() {
// 状态对象,用于存储前一个元素
class State {
T previous = null;
boolean first = true;
}
return Gatherer.<T, State, T>ofSequential(
State::new, // Initializer
(state, element, downstream) -> { // Integrator
if (state.first || !Objects.equals(state.previous, element)) {
state.previous = element;
state.first = false;
return downstream.push(element); // 推送非重复元素
}
return true; // 继续处理,但不推送重复元素
}
);
}
public static void main(String[] args) {
List<String> result = Stream.of("a", "a", "b", "c", "c", "c", "a", "d")
.gather(distinctAdjacent())
.toList();
System.out.println(result); // 输出: [a, b, c, a, d]
}
}此示例创建了一个
Gatherer
,它只传递与前一个元素不同的元素 [18]。
Stream Gatherers 为开发者提供了强大的工具来定制流处理逻辑,使得 Stream API 更加灵活和富有表现力。
2. JEP 484: Class-File API
JEP 484 提供了一个用于解析、生成和转换 Java 类文件的标准 API [5]。此 API 位于 java.lang.classfile
包及其子包中,经过 JDK 22 (JEP 457) 和 JDK 23 (JEP 466) 的预览后,在 JDK 24 中最终确定 [21]。
目标与动机:
- 为需要以编程方式操作类文件的框架、库和工具(如字节码操作库 ASM、ByteBuddy,或编译器、分析器)提供一个官方支持的、现代的 API。
- 取代 JDK 内部的、非标准的类文件处理能力。
- API 设计原则包括:不可变对象、树状结构表示、用户驱动导航、懒加载、统一的流式和物化视图、自动处理底层细节(如常量池、栈图)、并利用现代 Java 语言特性(如 lambda、record、sealed class、模式匹配)[21]。
核心概念与代码示例: API 主要围绕三个核心抽象:元素 (Elements)、构建器 (Builders) 和转换 (Transforms)。
-
元素 (Elements): 类文件各部分的不可变描述,如指令、属性、字段、方法。
-
构建器 (Builders): 用于创建复合元素,如
ClassBuilder
、MethodBuilder
、CodeBuilder
。 -
转换 (Transforms): 表示将一个元素转换为其他元素的函数。
-
生成类文件示例 (HelloWorld): 以下示例展示了如何使用 Class-File API 生成一个简单的 HelloWorld 类,该类包含一个打印 “Hello, Class-File API!” 的 main 方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.lang.classfile.*;
import java.lang.classfile.attribute.SourceFileAttribute;
import static java.lang.classfile.ClassFile.*; // 静态导入 Opcode, AccessFlags 等
import static java.lang.constant.ConstantDescs.*; // 静态导入 CD_System, CD_PrintStream 等
public class GenerateHelloWorld {
public static void main(String[] args) throws IOException {
String className = "HelloWorldGenerated";
Path classFilePath = Path.of(className + ".class");
byte[] classBytes = ClassFile.of().build(ClassDesc.of(className), classBuilder -> {
classBuilder.withFlags(ACC_PUBLIC | ACC_SUPER); // public class HelloWorldGenerated
classBuilder.with(SourceFileAttribute.of(className + ".java"));
// 添加默认构造函数 public HelloWorldGenerated()
classBuilder.withMethodBody(INIT_NAME, MTD_void, ACC_PUBLIC, code -> code
.aload(0) // this
.invokespecial(CD_Object, INIT_NAME, MTD_void) // super()
.return_()
);
// 添加 public static void main(String args)
classBuilder.withMethodBody("main", MethodTypeDesc.of(CD_void, CD_String.arrayType()),
ACC_PUBLIC | ACC_STATIC, code -> code
.getstatic(CD_System, "out", CD_PrintStream) // System.out
.ldc("Hello, Class-File API!") // 加载字符串常量
.invokevirtual(CD_PrintStream, "println", MethodTypeDesc.of(CD_void, CD_String)) // out.println(...)
.return_()
);
});
Files.write(classFilePath, classBytes);
System.out.println("Generated class file: " + classFilePath.toAbsolutePath());
}
}编译并运行
GenerateHelloWorld
后,会生成HelloWorldGenerated.class
文件。可以使用java HelloWorldGenerated
来执行它,输出 “Hello, Class-File API!” [21]。 -
转换类文件示例 (重命名方法中的类引用): 假设我们想将一个类中所有对
Foo
类的调用重定向到Bar
类。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// 假设我们有 classBytes (byte[] of a class to transform)
ClassFile cf = ClassFile.of();
ClassModel classModel = cf.parse(classBytes);
CodeTransform codeTransform = (codeBuilder, codeEntry) -> {
if (codeEntry instanceof InvokeInstruction instr
&& instr.owner().asInternalName().equals("Foo")) {
codeBuilder.invoke(instr.opcode(), ClassDesc.of("Bar"),
instr.name().stringValue(), instr.typeSymbol(), instr.isInterface());
} else {
codeBuilder.accept(codeEntry);
}
};
MethodTransform methodTransform = MethodTransform.transformingCode(codeTransform);
ClassTransform classTransform = ClassTransform.transformingMethods(methodTransform);
byte[] newBytes = cf.transform(classModel, classTransform);
Files.write(Path.of("TransformedClass.class"), newBytes);此示例(概念性,具体实现细节参考 JEP 文档)展示了如何定义转换逻辑,并将其应用于类模型以生成新的类文件字节 [21]。
Class-File API 为 Java 生态系统中的工具开发者提供了一个强大且标准化的方式来处理字节码,有望促进相关工具的创新和发展。
五、性能、运行时和工具增强 (Performance, Runtime, and Tooling Enhancements)
JDK 24 在 JVM 性能、垃圾收集、启动速度以及并发模型方面带来了多项重要改进。
1. JEP 450: 紧凑对象头 (实验性)
JEP 450 提议在 64 位架构的 HotSpot JVM 中将对象头的大小从 96-128 位减少到 64 位 [4]。这是一个实验性特性,默认禁用 [16]。
目标与影响:
- 减少堆大小: 对象头变小直接导致 Java 堆中对象占用的内存减少。
- 提高部署密度: 内存占用降低有助于在相同硬件上部署更多应用实例。
- 改善数据局部性: 更紧凑的对象可能带来更好的缓存利用率和数据局部性。
此特性灵感来源于 Project Lilliput [16]。由于是实验性的,启用它可能会导致意外后果,需要谨慎测试 [16]。
2. JEP 475: G1 的晚期屏障扩展
JEP 475 旨在简化 G1 垃圾收集器屏障的实现 [4]。G1 的写屏障用于记录应用程序对内存的访问,以便进行并发标记。此 JEP 将屏障代码的扩展从 C2 JIT 编译管道的早期阶段移至后期。
目标与影响:
- 简化 G1 实现: 减少 G1 实现的复杂性。
- 潜在的性能改进: 虽然主要目标是简化,但更晚的扩展可能为编译器优化提供更多机会,或减少编译时开销。
3. JEP 483: Ahead-of-Time 类加载与链接
JEP 483 旨在通过在 HotSpot JVM 启动时使应用程序的类以已加载和链接的状态立即可用来改善启动时间 [2]。
实现方式与影响:
- 通过在一次运行期间监控应用程序,并将所有已加载和链接的类的形式存储在缓存中,供后续运行使用 [4]。
- 这避免了每次应用程序启动时重复加载、验证和链接类文件的开销,从而减少“预热”时间并提高启动性能 [2]。
- 此特性是更广泛的 Project Leyden 的一部分,该项目旨在全面解决 Java 应用程序启动时间问题 [2]。它建立在 JDK 11 中引入的应用程序类数据共享 (AppCDS) 的基础上 [2]。
- 为未来进一步改进启动时间和预热时间奠定基础 [4]。
4. JEP 491: 无需固定的虚拟线程同步
JEP 491 旨在通过安排在 synchronized 方法和语句中阻塞的虚拟线程释放其底层平台线程,以供其他虚拟线程使用,从而提高使用这些构造的 Java 代码的可伸缩性 [2]。这将消除几乎所有虚拟线程被“固定”到平台线程的情况,这种固定会严重限制可用于处理应用程序工作负载的虚拟线程数量 [4]。
背景与动机: 虚拟线程(JEP 444, JDK 21 引入)旨在提高通常使用“每请求一线程”模型的应用程序的可伸缩性。然而,当虚拟线程在 synchronized
块或方法内阻塞时,它会“固定”其承载的平台线程,阻止该平台线程被其他虚拟线程使用 [2]。这是因为 synchronized
使用的监视器(monitor)与平台线程关联,而不是虚拟线程 [2]。
主要变更与影响:
- JEP 491 修改了 JVM 对
synchronized
关键字的实现,将监视器与虚拟线程关联,而不是平台线程 [2]。 - 虚拟线程现在可以在
synchronized
方法或语句内部、或在等待监视器时,自由地挂载(mount)和卸载(unmount)其承载平台线程 [24]。 - 当虚拟线程因获取监视器或调用
Object.wait()
而阻塞时,它将卸载并释放其承载平台线程 [24]。 - 显著增强了虚拟线程的可伸缩性,使得大量现有使用
synchronized
的代码能够更好地利用虚拟线程带来的优势,而无需重构为使用java.util.concurrent.locks
[2]。 - 对于新代码,开发者可以根据便利性和功能需求在
synchronized
和java.util.concurrent.locks
之间进行选择,而不再主要因为 pinning 问题而避免synchronized
[24]。
代码影响示例:
1 | // 之前可能导致 pinning 的代码 |
在 JEP 491 之前,如果 accessResource 方法中的操作导致虚拟线程阻塞,承载的平台线程也会被阻塞。JEP 491 之后,虚拟线程在阻塞时可以释放平台线程,从而允许更多虚拟线程并发执行,即使它们都使用了 synchronized 方法 [24]。
5. JEP 493: 无需 JMOD 的运行时镜像链接
JEP 493 允许 jlink 工具创建自定义运行时镜像,而无需使用 JDK 的 JMOD 文件 [1]。这可以通过在构建 JDK 时启用一个特定选项来实现,并且可能使 JDK 的大小减少约 25% [3]。
目标与影响:
- 减小 JDK 大小: 对于希望分发更小 JDK 或自定义运行时的开发者和供应商来说,这是一个显著的改进。
- 提高效率: 简化了创建自定义运行时的过程。
- 需要注意的是,此特性需要在构建 JDK 时启用,并非所有 JDK 供应商都可能选择启用它 [3]。
六、安全增强 (Security Enhancements)
JDK 24 在安全性方面也做出了重要更新,包括移除过时的安全机制和为后量子密码时代做准备。
1. JEP 486: 永久禁用安全管理器
JEP 486 永久禁用了自 Java 1.0 以来就存在的安全管理器 (Security Manager) [1]。安全管理器最初设计用于在执行远程加载的代码(如 Applet)时提供沙箱环境,但早已不再是客户端 Java 的主要安全手段,也很少用于保护服务器端代码 [2]。它在 JDK 17 中已被弃用。
影响:
- 移除过时组件: 减少了 JDK 的维护负担。
- 迁移挑战: 依赖安全管理器的应用程序需要进行重大的架构更改和代码重写才能迁移到 JDK 24 及更高版本,因为没有提供直接的替代方案 [2]。
- Applet 和浏览器插件已在 JDK 11 中移除,进一步降低了安全管理器的相关性 [2]。
2. JEP 496: 抗量子模块化格密码密钥封装机制
JEP 496 通过提供抗量子模块化格密码密钥封装机制 (ML-KEM, Module-Lattice-based Key-Encapsulation Mechanism) 的实现来增强 Java 应用程序的安全性 [1]。ML-KEM 已由 NIST 标准化为 FIPS 203。
目标与影响:
- 后量子密码准备: 这是 Java 平台为应对未来量子计算机对现有公钥密码体系构成威胁而迈出的重要一步 [10]。
- 安全密钥交换: KEM 用于通过公钥密码在不安全的通信渠道上安全地交换对称密钥 [3]。
- 将通过实现 Java
KeyPairGenerator
、KEM
和KeyFactory
类来完成 [27]。
3. JEP 497: 抗量子模块化格密码数字签名算法
JEP 497 实现了抗量子的模块化格密码数字签名算法 (ML-DSA, Module-Lattice-Based Digital Signature Algorithm),该算法已由 NIST 标准化为 FIPS 204 [1]。
目标与影响:
- 增强数据完整性和身份验证: 数字签名用于检测数据是否被未经授权地修改,并验证签名者的身份 [3]。
- 后量子密码支持: 与 JEP 496 一样,这是 Java 平台为支持后量子密码 (PQC) 所做的重要准备 [10]。
- 将通过实现 Java
KeyPairGenerator
、Signature
和KeyFactory
类来完成 [16]。
4. JEP 498: 警告使用 sun.misc.Unsafe 中的内存访问方法
JEP 498 规定,当使用 sun.misc.Unsafe
中已弃用的内存访问方法时,JVM 将发出警告 [1]。这些方法计划在未来版本中被完全移除。
目标与影响:
- 推动迁移: 鼓励开发者从不安全的、内部的
sun.misc.Unsafe
API 迁移到标准的、受支持的 API,如 VarHandle API 和 Foreign Function & Memory API (FFM API) [2]。 - 提高安全性与稳定性: 减少对内部 API 的依赖有助于提高 Java 应用程序的长期稳定性和安全性。
七、孵化器和实验性特性 (Incubator and Other Experimental Features)
除了预览特性,JDK 24 还包含孵化阶段和进一步的实验性特性。
1. JEP 489: Vector API (第九次孵化)
Vector API 旨在提供一种表达向量计算的方式,这些计算能够在运行时可靠地编译为支持的 CPU 架构上的最佳向量指令,从而实现优于等效标量计算的性能 [16]。此 JEP 是该 API 的第九次孵化,整合了前八轮孵化的反馈。
目标与影响:
- 提升计算密集型应用性能: 特别适用于科学计算、机器学习、数据分析等领域。
- 持续演进: Vector API 将继续孵化,直到 Project Valhalla 的必要特性作为预览特性可用。届时,Vector API 将适配这些特性并从孵化阶段提升到预览阶段 [16]。
2. JEP 404: 分代 Shenandoah (实验性)
JEP 404 提议为 Shenandoah 垃圾收集器提供一个实验性的分代模式,目标是在未来的 JDK 版本中使其成为默认模式,同时不破坏非分代 Shenandoah GC [1]。该 JEP 最初计划用于 JDK 21,但由于审查过程中发现的风险和时间不足而推迟 [16]。
目标与影响:
- 改进 GC 性能: 分代垃圾收集通常可以为具有特定分配模式的应用程序(例如,大量短暂对象)提供更好的吞吐量和更低的停顿时间。
- Shenandoah 的演进: 这是 Shenandoah GC 发展的一个重要方向,旨在结合其低停顿特性和分代收集的优势。
八、核心库更新 (Core Library Updates)
除了 JEPs 驱动的主要特性外,JDK 24 的核心库也进行了一些更新。
1. Unicode 支持更新 (Unicode Support Updates)
java.lang.Character
类支持 Unicode 字符数据库 16.0,新增 5,185 个字符,总计达到 154,998 个字符。新增内容包括七个新脚本,如 Garay(西非现代用脚本)、Gurung Khema、Kirat Rai、Ol Onal、Sunuwar(印度东北部和尼泊尔的现代用脚本)、Todhri(阿尔巴尼亚历史脚本)和 Tulu-Tigalari(印度西南部历史脚本)[3]。java.text.Bidi
和java.text.Normalizer
类分别支持 Unicode 标准附件 #9 和 #15 [4]。java.util.regex
包支持基于 Unicode 标准附件 #29 的扩展字形集群 [4]。
影响: 这些更新增强了 Java 对全球各种语言和字符集的国际化支持,确保了与最新 Unicode 标准的兼容性。
2. 其他API新增/增强 (Other Minor API Additions/Enhancements)
JDK 24 还包含一些实用性的 API 新增和增强 [3]:
-
Reader.of(CharSequence): 新增静态方法,用于创建一个从给定 CharSequence 读取字符的 Reader 实例。
-
Process.waitFor(Duration): Process 类新增了带有超时参数的 waitFor 方法,允许在等待进程终止时指定一个最长等待时间。
-
jar 工具增强:
- 新增
-C <dir>
选项,允许在提取 (-x
或--extract
) JAR 文件时指定目标目录。 - 新增
--no-overwrite
选项,用于在提取时防止覆盖现有文件。
- 新增
-
TLS 密码套件禁用机制: 现在可以通过在
java.security
配置文件的jdk.tls.disabledAlgorithms
安全属性中使用通配符(例如 “TLS_RSA_*”)来禁用匹配模式的 TLS 密码套件。 -
虚拟线程监控与管理:
- 新增 MXBean (
jdk.management.neuesten.VirtualThreadMXBean
) 用于监视和管理虚拟线程调度器。 - 新增 jcmd 命令
Thread.vthread_scheduler
和Thread.vthread_pollers
,提供对虚拟线程调度和轮询器状态的诊断。
- 新增 MXBean (
-
安全属性文件包含: 支持在安全属性文件中包含其他属性文件。
影响: 这些更新虽然不像 JEP 那样引人注目,但它们为开发者提供了更多便利,增强了 JDK 的可管理性和可观察性,并改进了特定场景下的开发体验。
九、总结与展望 (Conclusion and Outlook)
JDK 24 作为一个重要的版本,通过其包含的 24 个 JEPs,在多个方面推动了 Java 平台的进步。
JDK 24 主要贡献回顾
此版本显著提升了开发者生产力,特别是通过语言层面的简化(如简单源文件和实例主方法、灵活的构造函数体、模块导入声明的预览)和 Stream API 的增强(Stream Gatherers)。在性能方面,Ahead-of-Time 类加载与链接、无需固定的虚拟线程同步以及对 G1 和 Shenandoah GC 的改进,都旨在优化 Java 应用的启动速度、运行时效率和可伸缩性。安全性方面,永久禁用安全管理器标志着一个时代的结束,而抗量子密码算法的引入则为 Java 的未来安全奠定了基础。
对开发者的关键益处
- 更低的入门门槛和更简洁的编程体验: 对于初学者和编写小型实用程序而言,Java 变得更加友好。
- 并发应用的可伸缩性提升: 依赖
synchronized
的现有并发应用程序在迁移到虚拟线程时,有望获得更好的性能和伸缩性。 - 更强大的数据处理能力: Stream Gatherers 提供了前所未有的灵活性来定制流操作。
- 标准化的字节码操作: Class-File API 为工具开发者提供了坚实的基础。
- 面向未来的安全性: 为应对量子计算带来的挑战做好了初步准备。
展望未来
JDK 24 中的许多预览特性,如模式匹配中的基本类型、灵活构造函数体、模块导入声明、简单源文件、作用域值以及 Vector API,都有望在未来的 JDK 版本中(如 JDK 25 [28])得到进一步完善并最终确定。Project Loom(虚拟线程、结构化并发、作用域值)、Project Valhalla(值对象、基本类型泛型)、Project Leyden(静态镜像、优化启动)和 Project Lilliput(紧凑对象头)等长期项目将继续驱动 Java 平台的演进。Java 社区可以期待在语言特性、性能优化、并发模型和平台安全性等方面的持续创新和改进。JDK 24 的发布再次证明了 Java 平台致力于满足现代应用开发需求的承诺。
引用的著作
- JDK 24 - OpenJDK, 访问时间为 五月 28, 2025, https://openjdk.org/projects/jdk/24/
- Six JDK 24 Features You Should Know About - Azul Systems, 访问时间为 五月 28, 2025, https://www.azul.com/blog/six-jdk-24-features-you-should-know-about/
- The Arrival of Java 24 - Oracle Blogs, 访问时间为 五月 28, 2025, https://blogs.oracle.com/java/post/the-arrival-of-java-24
- JDK 24 Release Notes, Important Changes, and Information - Oracle, 访问时间为 五月 28, 2025, https://www.oracle.com/java/technologies/javase/24-relnote-issues.html
- Consolidated JDK 24 Release Notes - Oracle, 访问时间为 五月 28, 2025, https://www.oracle.com/java/technologies/javase/24all-relnotes.html
- JEP 488: Primitive Types in Patterns, instanceof, and switch (Second …), 访问时间为 五月 28, 2025, https://openjdk.org/jeps/488
- Java Language Changes Summary - Oracle Help Center, 访问时间为 五月 28, 2025, https://docs.oracle.com/en/java/javase/24/language/java-language-changes-summary.html
- JEP: Flexible Constructor Bodies. Final for JDK 25 : r/java - Reddit, 访问时间为 五月 28, 2025, https://www.reddit.com/r/java/comments/1k4fm3i/jep_flexible_constructor_bodies_final_for_jdk_25/
- JEP 492: Flexible Constructor Bodies (Third Preview) - OpenJDK, 访问时间为 五月 28, 2025, https://openjdk.org/jeps/492
- Oracle Releases Java 24, 访问时间为 五月 28, 2025, https://www.oracle.com/news/announcement/oracle-releases-java-24-2025-03-18/
- JEP 494: Module Import Declarations (Second Preview) - OpenJDK, 访问时间为 五月 28, 2025, https://openjdk.org/jeps/494
- Module Import Declarations - Oracle Help Center, 访问时间为 五月 28, 2025, https://docs.oracle.com/en/java/javase/24/language/module-import-declarations.html
- JEP 495: Simple Source Files and Instance Main Methods (Fourth …), 访问时间为 五月 28, 2025, https://openjdk.org/jeps/495
- JEP 487: Scoped Values (Fourth Preview) - OpenJDK, 访问时间为 五月 28, 2025, https://openjdk.org/jeps/487
- Scoped Values - Oracle Help Center, 访问时间为 五月 28, 2025, https://docs.oracle.com/en/java/javase/24/core/scoped-values.html
- Java 24 Delivers New Experimental and Many Final Features - InfoQ, 访问时间为 五月 28, 2025, https://www.infoq.com/news/2025/03/java24-released/
- JEP 485: Stream Gatherers - OpenJDK, 访问时间为 五月 28, 2025, https://openjdk.org/jeps/485
- Stream Gatherers (JEP 485) - javaalmanac.io, 访问时间为 五月 28, 2025, https://javaalmanac.io/features/gatherers/
- Liberica JDK 24 builds are generally available - BellSoft, 访问时间为 五月 28, 2025, https://bell-sw.com/blog/liberica-jdk-24-is-released/
- JEP proposed to target JDK 24: 484: Class-File API - OpenJDK mailing lists, 访问时间为 五月 28, 2025, https://mail.openjdk.org/pipermail/jdk-dev/2024-October/009427.html
- JEP 484: Class-File API - OpenJDK, 访问时间为 五月 28, 2025, https://openjdk.org/jeps/484
- A Basic Introduction to the Classfile API | Ife Sunmola, 访问时间为 五月 28, 2025, https://ifesunmola.com/a-basic-introduction-to-the-classfile-api/
- mrjameshamilton/java-class-file-api-hello-world - GitHub, 访问时间为 五月 28, 2025, https://github.com/mrjameshamilton/jep457-hello-world
- JEP 491: Synchronize Virtual Threads without Pinning - OpenJDK, 访问时间为 五月 28, 2025, https://openjdk.org/jeps/491
- Java Evolves to Tackle Virtual Threads Pinning with JEP 491 - InfoQ, 访问时间为 五月 28, 2025, https://www.infoq.com/news/2024/11/java-evolves-tackle-pinning/
- Re: Apache Tomcat 12+, 访问时间为 五月 28, 2025, https://lists.apache.org/thread/9pq3t6gsqh24tol24mnk6hy1hz8m7mm6
- OpenJDK News Roundup: Instance Main Methods, Flexible Constructor Bodies, Module Import Declarations - InfoQ, 访问时间为 五月 28, 2025, https://www.infoq.com/news/2024/11/jdk-news-roundup-nov04-2024/
- Java News Roundup: JDK 24, GraalVM for JDK 24, Payara Platform, Kafka 4.0, Spring CVEs, JavaOne 2025 - InfoQ, 访问时间为 五月 28, 2025, https://www.infoq.com/news/2025/03/java-news-roundup-mar17-2025/# JDK 24 新特性深度解析与代码示例