JDK 24中全新24个JEP介绍

JDK 24 的全部 24 个新 JEP:抗量子加密、垃圾收集器和更多清理:


JEP 485:流收集器
JEP 485 扩展了 Java 的 Stream API,使其能够定义自定义中间操作(称为收集器)。这允许在流中进行更灵活、更具表现力的数据处理,从而实现以前使用内置中间操作难以或无法实现的转换。

它解决的问题:现有的 Stream API 提供了一组固定的中间操作,如映射、过滤和排序。但是,它缺乏一种机制(类似于现有的 Collectors)来定义更复杂的操作,例如将元素分组到固定大小的窗口中或根据自定义标准删除重复项。因此,开发人员经常不得不求助于复杂的解决方法或放弃流而采用迭代代码(哇!)。

提供的解决方案:流收集器允许开发人员为流定义自定义中间操作。这些收集器可用于有限和无限流,并支持顺序和并行处理。这使得流更加通用,能够适应特定的应用程序需求(或特定开发人员的想法)。

代码示例:

import java.util.stream.Stream;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("a", "b", "c", "d", "e");
        var result = stream.collect(Gatherers.slidingWindow(3))
                           .map(window -> window.collect(Collectors.toList()))
                           .collect(Collectors.toList());

        System.out.println(result);
       
// Output: [[a, b, c], [b, c, d], [c, d, e]]
    }
}

上述代码演示了如何使用自定义收集器 sliderWindow,它将流元素分组到指定大小的窗口中,从而实现高级流操作。有关详细机制,请查看 JEP 本身;这里我们重点介绍它的用法。

PS: 关于 Stream Gatherers API 的更多信息可以在这里找到。

JEP 484:类文件 API
JEP 484 旨在提供用于解析、生成和转换 Java 类文件的标准 API。此 API 允许开发人员和工具根据 JVM 规范中定义的格式处理类文件,从而促进新语言和 VM 功能的采用。

解决的问题:

Java 生态系统有许多用于处理类文件的库,例如 ASM、BCEL 和 Javassist。然而,类文件格式的快速演变(与 Java 的动态开发和六个月的 JDK 发布周期相关)导致了兼容性问题。类文件格式会随着每个 JDK 版本的发布而变化,引入新的属性、指令或元数据。如果库(例如 ASM)尚未更新以支持最新的 JDK,则分析或操作此类类文件的尝试可能会失败。

此外,JDK 包含用于处理类文件的内部库,这降低了生态系统采用新功能的能力。在“稳定”的 Java 版本包含更新之前,新库版本通常会延迟发布。


JEP提供的解决方案:

引入一个随着类文件格式发展而发展的处理类文件的标准 API,使得平台组件和外部工具或库能够立即支持最新的语言和 VM 功能。

此 API 将类文件元素表示为不可变对象,镜像类文件的层次结构,支持延迟处理,并提供类文件的流式和物化视图。这使开发人员能够高效地处理类文件,同时使 JDK 组件能够逐步迁移到标准 API。将来,这可能会允许删除外部库(例如 ASM)的内部副本。

代码示例:

public class ClassFileExample {
    public static void main(String[] args) throws Exception {

        Path classFilePath = Path.of("MyClass.class");

        byte[] classData = Files.readAllBytes(classFilePath);

        ClassModel classModel = ClassModel.parse(classData);

        List<MethodModel> methods = classModel.methods();

        for (MethodModel method : methods) {
            System.out.println(
"Method: " + method.methodName());
        }
    }
}

上面的代码演示了新 API 如何读取类文件、解析它并显示该类中包含的所有方法的名称。

Liliputs、垃圾收集器和虚拟线程 - VM 内部

JEP 404:分代 Shenandoah(实验性)
就像之前的 JDK 版本引入了分代 ZGC 一样,JEP 404 旨在通过添加实验性的分代功能来增强 Shenandoah 垃圾收集器(一种低延迟算法,可执行并行和并发垃圾收集,对应用程序响应时间的影响最小)。此更改旨在提高吞吐量、对突发负载的弹性和内存效率。

它解决了什么问题:

传统的分代收集器(如 G1 或 CMS)假设大多数对象很快就会变得不再使用,因此会将精力集中在较新的对象上。非分代 Shenandoah 需要更多内存和大量工作来从无法访问的对象中回收空间,这可能会导致应用程序暂停时间更长。

解决方案是什么:

Shenandoah 引入了分代模式,将堆分为两代:年轻代和老代。这样,收集器就可以专注于从年轻代回收内存,年轻代中的大多数对象很快就会变得不再使用,从而缩短暂停时间并改善内存管理。

JEP 475:G1 的后期屏障扩展
JEP 475 的目标是通过将垃圾收集器屏障的扩展转移到 C2 编译器编译过程的后期阶段来简化 G1 垃圾收集器屏障的实现。这一变化旨在减轻编译器负担,并且(有趣的是)使代码更易于 JVM 开发人员理解和维护。

它解决了什么问题:

在目前的实现中,G1 屏障在编译过程的早期就被展开,这增加了中间代码的复杂性并增加了 C2 编译器的负担。这导致编译时间更长,并且难以保持内存操作的正确顺序,从而可能导致错误和代码维护困难。

解决方案是什么:

所提出的解决方案将 G1 屏障的扩展延迟到编译过程的后期阶段,即在生成机器代码之前。这样一来,屏障就可以在中间代码中以更紧凑的形式表示,从而减轻编译器的负担并帮助保持正确的操作顺序。此外,这种方法可以实现更好的优化并简化屏障实现,从而提高性能并简化代码维护。

JEP 490:ZGC:删除 JEP 的非生成模式链接
JEP 490 的目标是移除 HotSpot 虚拟机中的 Z 垃圾收集器 (ZGC) 的非分代模式。这一改变旨在通过仅关注 ZGC 的分代模式来简化 ZGC 代码并降低维护成本。

它解决了什么问题:为 ZGC 维护两种独立的操作模式 - 分代和非分代 - 增加了代码库的复杂性并增加了开发和维护的负担。分代 ZGC 模式已被引入作为大多数用例的更好解决方案,使得非分代模式变得不那么必要。

解决方案是什么:JEP 490 建议通过弃用 ZGenerational 选项并删除与此模式相关的代码和测试来删除 ZGC 的非分代模式。在未来的版本中,此选项将被彻底删除,尝试使用它将导致运行 JVM 时出错。

肯定有一些事情让人兴奋。

但你永远不会像垃圾收集器本身那样兴奋!
我们已经完成了垃圾收集,现在是时候进行虚拟线程和内存使用了!

JEP 491:无需固定即可同步虚拟线程
JEP 491 的目标是通过允许在此类构造中阻塞的虚拟线程释放其分配的平台线程,从而允许其他虚拟线程使用它们,从而提高使用同步方法和块的代码的可扩展性。此更改消除了几乎所有将虚拟线程“固定”到平台线程的情况,这之前会限制可用于处理应用程序负载的虚拟线程数量。

它解决了什么问题?

Java 21 中引入的虚拟线程由 JDK 而非操作系统管理,因此可以使用大量虚拟线程创建高吞吐量应用程序。但是,当虚拟线程在同步方法或块内执行代码并遇到阻塞操作(例如从套接字读取)时,它无法从其平台线程“取消固定”。这会导致虚拟线程“固定”到平台线程,从而限制应用程序的可扩展性,因为阻塞的平台线程无法处理其他虚拟线程。

解决方案是什么?

JEP 491 提议修改 JVM 的对象监视器实现,以便虚拟线程可以独立地获取、保留和释放监视器,而不受其平台线程对应方的影响。当虚拟线程在同步块或方法内阻塞时,平台线程可以被释放以处理其他虚拟线程,从而显著提高应用程序的可扩展性。

此外,通过扩展 JDK Flight Recorder 中记录 jdk.VirtualThreadPinned 事件的场景,将增强诊断功能,从而更容易识别虚拟线程无法释放平台线程的情况。

代码示例:

synchronized byte[] getData() {
    byte[] buf = ...;
    int nread = socket.getInputStream().read(buf); // Could block here
}

在上面的例子中,如果 read 方法由于缺少可用字节而阻塞,则执行 getData 的虚拟线程将能够从其平台线程“解除锁定”,从而释放平台线程来处理其他任务。这将为使用同步方法和虚拟线程块的应用程序提供更好的可扩展性。

JEP 450:紧凑对象标头(实验性)
Liliput 项目的一个子项目。JEP 450 的目标是将 HotSpot 虚拟机中的对象头大小从 96-128 位减小到 64 位架构上的 64 位,旨在减少内存使用量、提高部署密度并增强数据局部性。

它解决了什么问题?

在当前的 JVM 实现中,对象头会占用大量堆内存,尤其是在处理小对象的应用程序中。减小对象头的大小可以更有效地利用内存,并减少内存管理的开销。

解决方案是什么?

内存中的每个对象都由数据和“元数据”组成——有关对象是什么以及它如何表现的信息。

在此背景下:

标记字是对象内部的一部分数据,用于存储技术信息,例如该对象是否被锁定(由线程锁定),或者垃圾收集器是否正在处理它等。

Class 词包含有关对象“类型”(或类)的信息,使程序能够理解,例如“这是一个 Cat 对象,而不是 Dog”。

每个元素都占用特定数量的内存。

建议的解决方案是将两个头字(标记字和类字)合并为一个 64 位字,方法是压缩类指针并将其放在标记字内。这需要修改锁定机制、垃圾收集和类指针处理,以保持对象类型信息的完整性和可用性。

如果您想测试对应用程序的影响,可以使用以下命令在 JVM 启动期间激活紧凑对象头:

java -XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders -jar application.jar
上述命令将允许您评估此选项对特定环境中的性能和内存使用情况的影响。

Java 的量子飞跃:安全性和加密

JEP 478:密钥派生函数 API(预览)
JEP 478 引入了密钥派生函数 (KDF) API,这是一种加密算法,旨在从密钥和其他数据生成其他密钥。此 API 允许应用程序使用 KDF 算法,例如基于 HMAC 的提取和扩展密钥派生函数 (HKDF) 和 Argon2。它还为安全提供商提供了一个框架,以使用 Java 或本机代码实现这些算法。

它解决的问题:

随着包括量子计算在内的技术进步,传统加密算法越来越容易受到攻击。Java 平台中缺乏标准 KDF API,这让旨在抵御量子攻击的现代加密协议(如混合公钥加密 (HPKE))的实现变得复杂。此外,现有 API 缺乏一种自然的方式来表示高级 KDF 功能,限制了开发人员实现和认证自定义 KDF 算法。

解决方案:

引入该类javax.crypto.KDF作为密钥派生函数的标准接口简化了各种 KDF 算法的使用和实现。该 API 支持使用适当参数进行初始化,并有助于密钥或数据派生。这可确保共享加密输入知识的各方之间安全且可重复地生成密钥。该 API 还为 Java 中的后量子密码学支持奠定了基础。

代码示例:

KDF kdf = KDF.getInstance("HKDF");

SecretKey derivedKey = kdf.deriveKey(
"AES", new AESKeyGenParameterSpec(256));

JEP 496:基于抗量子模块格的密钥封装机制
JEP 496 实现了一种基于模块化格子网络 (ML-KEM) 的抗量子密钥封装机制。密钥封装机制 (KEM) 使用公钥加密技术保护通过不受保护的通信通道传输的对称密钥。ML-KEM 旨在抵御未来的量子攻击,并已由 NIST 在 FIPS 203 规范中进行了标准化。

它解决了什么问题?

量子计算的进步对目前使用的加密算法(如 RSA 和 Diffie-Hellman)构成了威胁,理论上,这些算法可以通过 Shor 算法破解。这迫切需要抗量子算法来保护数据免受未来威胁。由 NIST 标准化的 ML-KEM 提供了这种保护,并有助于保护应用程序免受潜在的量子攻击。

解决方案:

JEP 496 建议使用适当的 Java API 实现 ML-KEM:

  • KeyPairGenerator 用于生成 ML-KEM 密钥对。
  • KEM 用于基于 ML-KEM 密钥对协商共享密钥。
  • KeyFactory 用于在 ML-KEM 密钥和编码格式之间进行转换。

该规范引入了 Java 中名为“ML-KEM”的新型安全算法系列。FIPS 203 为 ML-KEM 定义了三个参数集:ML-KEM-512、ML-KEM-768 和 ML-KEM-1024,它们的安全强度和性能各不相同。

代码示例:

// Generating an ML-KEM key pair
KeyPairGenerator generator = KeyPairGenerator.getInstance(
"ML-KEM");
generator.initialize(NamedParameterSpec.ML_KEM_512);
KeyPair keyPair = generator.generateKeyPair();

// Key encapsulation by the sender
KEM kem = KEM.getInstance(
"ML-KEM");
KEM.Encapsulator encapsulator = kem.newEncapsulator(keyPair.getPublic());
KEM.Encapsulated encapsulated = encapsulator.encapsulate();
byte[] encapsulationMessage = encapsulated.encapsulation();
// Message to send
SecretKey secretKeySender = encapsulated.key();
// Sender's secret key

// Key decapsulation by the receiver
KEM.Decapsulator decapsulator = kem.newDecapsulator(keyPair.getPrivate());

// Receiver's secret key
SecretKey secretKeyReceiver = decapsulator.decapsulate(encapsulationMessage); 

// secretKeySender and secretKeyReceiver contain the same secret key

JEP 497:基于模块格的抗量子数字签名算法
JEP 497 实现了基于模块化网格网络 (ML-DSA) 的抗量子数字签名算法系列。数字签名对于检测未经授权的数据修改和验证签名者的身份至关重要。ML-DSA 旨在抵御未来的量子攻击,并符合 NIST 的 FIPS 204 标准。

它解决的问题:见上文 - 量子进步威胁着传统算法。

解决方案:

除了上述 API 之外,还引入了一个名为“ML-DSA”的新安全算法系列。FIPS 204 为 ML-DSA 定义了三个参数集:ML-DSA-44、ML-DSA-65 和 ML-DSA-87,提供不同级别的安全性和性能。

代码示例:

// Generating an ML-DSA key pair
KeyPairGenerator generator = KeyPairGenerator.getInstance(
"ML-DSA");
generator.initialize(NamedParameterSpec.ML_DSA_44);
KeyPair keyPair = generator.generateKeyPair();

// Signing data
Signature signer = Signature.getInstance(
"ML-DSA");
signer.initSign(keyPair.getPrivate());
signer.update(data);
byte[] signature = signer.sign();

// Verifying signature
Signature verifier = Signature.getInstance(
"ML-DSA");
verifier.initVerify(keyPair.getPublic());
verifier.update(data);
boolean isValid = verifier.verify(signature);

在此示例中,使用 ML-DSA-44 参数集生成 ML-DSA 密钥对。私钥对数据进行签名,而公钥验证签名。

“我不想再和你玩了”——弃用

正在被删除的内容(或将来会被删除的内容)。

JEP 472:准备限制 JNI 的使用
JEP 472 引入了有关使用 Java 本机接口 (JNI) 的警告,并调整了外部函数和内存 (FFM) API 以发出一致警告。这为开发人员做好了迎接未来版本的 Java 的准备,在这些版本中,与本机代码的交互将默认受到限制,以确保平台完整性。

它解决的问题:

Java 代码与本机代码通过 JNI 进行的交互可能会损害应用程序的完整性,从而导致未定义的行为,例如 JVM 崩溃、在没有适当访问控制的情况下操纵字段或内存管理不当。引入警告可提高开发人员的意识,并让他们为即将到来的变化做好准备。

解决方案:

引入了针对 JNI 使用的警告机制,并对 FFM API 进行了调整以发出一致警告。开发人员可以通过使用 --enable-native-access 选项在运行时选择性地启用对本机接口的访问来绕过这些警告(以及潜在的未来限制)。

例子:

# Enable native interface access for all unnamed modules (classes from the classpath)
java --enable-native-access=ALL-UNNAMED -jar application.jar

# Enable native interface access for specific modules
java --enable-native-access=module1,module2 -jar application.jar

这些命令运行启用了指定模块的本机接口访问的应用程序,从而避免与 JNI 和 FFM API 使用相关的警告。

JEP 498:在 sun.misc.Unsafe 中使用内存访问方法时发出警告
JEP 498 在程序执行期间首次使用 sun.misc.Unsafe 类中的内存访问方法时发出警告。这些方法在 JDK 23 中已弃用,并计划在未来版本中删除。它们已被标准 API VarHandle(如 JEP 193、JDK 9)和外部函数和内存 API(JEP 454、JDK 22)取代。这些警告旨在鼓励开发人员从 sun.misc.Unsafe 迁移到受支持的替代方案,确保顺利过渡到现代 JDK 版本。

它解决的问题:

sun.misc.Unsafe 类于 2002 年推出,允许执行低级操作,主要用于内存访问。虽然这些方法功能强大,但它们并不安全,会导致未定义的行为和 JVM 崩溃。尽管它们旨在内部使用,但由于其性能优势,开发人员还是广泛采用了它们。然而,许多库未能实施足够的安全检查,增加了应用程序风险。在 JDK 9(JEP 260)中隐藏该类并提供替代方案的努力推迟了它的删除。

解决方案:

JEP 498 在第一次调用来自 sun.misc.Unsafe 的任何内存访问方法时引入了运行时警告。一个新的命令行选项

--sun-misc-unsafe-memory-access={allow|warn|debug|deny}

在 JDK 23 中添加了该选项来控制方法行为。其默认值(当前为 allow)将在 JDK 24 中更改为 warn,在首次使用时显示警告。

JEP 479:删除 Windows 32 位 x86 端口
JEP 479 删除了源代码以及在 32 位 Windows x86 系统上编译 JDK 的支持。宣布将在 JDK 21 中删除该功能,该功能的删除简化了 JDK 构建和测试基础架构,解决了技术债务问题。

它解决的问题:

维持对 32 位 Windows x86 的支持会消耗资源并使新功能的开发变得复杂。实现虚拟线程 (JEP 436) 依赖于内核线程,这在该平台上没有任何优势。此外,Windows 10 是最后一个支持 32 位操作的操作系统,将于 2025 年 10 月终止支持。

解决方案:

删除了 32 位 Windows x86 的特定代码路径,并修改了 JDK 构建系统,使其不再针对此平台进行编译。更新了文档以反映这些更改,简化了 JDK 构建和测试基础架构,并使 OpenJDK 社区专注于新功能和改进。

这是我使用的第一个 JDK。安息吧。


JEP 501:弃用 32 位 x86 端口并将其删除
JEP 501 将 32 位 x86 端口标记为弃用,并计划在未来的 JDK 版本中将其移除。目前,唯一支持的 32 位 x86 端口是 Linux。一旦移除,在 32 位 x86 处理器上运行 Java 程序的唯一方法将是通过独立于架构的 Zero 端口。

解决的问题:

维护 32 位 x86 端口会产生额外成本,尤其是针对 Loom、Foreign Function & Memory API 或 Vector API 等新功能实施平台特定解决方案或变通方法时。这会延迟 JDK 的创新和采用。

解决方案:

JEP 501 提议在 JDK 24 中弃用 32 位 x86 端口,并计划在 JDK 25 中将其完全删除。尝试为 32 位 x86 配置构建将导致错误,表明其已弃用。开发人员可以使用 --enable-deprecated-ports=yes 覆盖此设置,但不能保证编译成功或功能正常。

此次弃用是继 JEP 449(弃用 Windows 32 位 x86 端口)和 JEP 479(删除该端口)等先前举措之后的举措。随着对 32 位操作系统和 x86 处理器的支持逐渐减少,这一举措简化了 JDK 开发并使其现代化。

JEP 486:永久禁用安全管理器
JEP 486 永久禁用了 Java 平台中的安全管理器。安全管理器在早期 Java 版本中引入,现在不再是保护客户端代码的主要机制,并且很少用于服务器端代码。在 Java 17(JEP 411,2021)中,它被标记为删除,其弃用最初受到了批评,但现在已被广泛接受为必要步骤。


解决的问题:

安全管理器增加了 Java 库的复杂性,需要许多方法来检查资源访问权限。尽管有此意图,但它很少被使用,并且大多数授予完整权限的应用程序否定了其最小特权模型。维护安全管理器的成本很高,会分散开发现代安全机制的资源。

解决方案:

JEP 486 引入了:

删除了使用 -Djava.security.manager 选项在运行时启用安全管理器的能力。
禁止通过 System.setSecurityManager(...) 安装安全管理器。
简化了以前依赖安全管理器进行资源访问决策的数百个 JDK 类。

API 修改为好像安全管理器从未启用过一样。

这些变化允许从 JDK 代码中删除安全管理器,从而能够专注于实现现代安全功能,如新协议(例如 TLS 1.3、HTTP/3)和更强大的加密算法。

java -Djava.security.manager -jar application.jar

导致程序因严重错误而终止,指出无法再启用安全管理器。

开发人员应该更新应用程序以不依赖安全管理器,并考虑使用 Java 中可用的现代安全机制。

冷启动和构建时间改进

现在我们来谈谈旨在改善平台启动时间的优化。

JEP 483:提前类加载和链接
JEP 483 旨在通过使 HotSpot JVM 在启动期间立即访问预加载和预链接的类来缩短 Java 应用程序的启动时间。这是通过在运行期间监视应用程序并将所有类的加载和链接形式保存在缓存中以供后续运行使用来实现的。

换句话说,JDK 又获得了另一层动态缓存。

解决的问题:

Java 的动态功能(例如运行时类加载、动态绑定和反射)虽然功能强大,但会显著增加应用程序的启动时间。典型的服务器应用程序会在启动期间扫描数百个 JAR 文件、读取和解析数千个类文件、将数据加载到类对象中并将它们绑定在一起。这可能需要几秒到几分钟的时间 - 但公平地说,这种极端情况越来越少见。JEP 483 通过允许提前执行并缓存这些操作来解决此问题,从而缩短后续应用程序的启动时间。

解决方案:

该解决方案扩展了 HotSpot JVM,使其包含一个用于存储预加载和预链接类的缓存。该过程涉及以下步骤:

记录 AOT 配置:以记录模式运行应用程序,将 AOT 配置保存在文件(app.aotconf)中:
创建 AOT 缓存:使用记录的配置在文件 (app.aot) 中创建 AOT 缓存:
使用 AOT 缓存:在后续运行中,使用创建的缓存来加快应用程序启动速度:

通过利用此缓存,JVM 可以立即访问预加载和预链接的类,从而显著缩短应用程序启动时间 — — JDK 开发人员就是这样承诺的。我们必须等待基准测试来验证这些说法。

JEP 493:无需 JMOD 即可链接运行时图像
JEP 493 旨在通过启用 jlink 工具来创建自定义运行时映像(无需使用 JMOD 文件),从而将 JDK 大小减少约 25%。此功能必须在 JDK 构建时启用;默认情况下不处于活动状态,某些 JDK 供应商可能选择不包含此功能。

它解决的问题:完整的 JDK 安装包括两个主要组件:

运行时映像:可执行的 Java 运行时系统。

JMOD 文件:一种模块化格式,包含运行时映像中每个模块的类文件、本机库、配置文件和其他资源。

jlink 使用 JMOD 文件来创建自定义运行时映像。但是,它们的存在会重复运行时映像中已有的所有文件,从而浪费大量空间。JMOD 文件约占 JDK 总大小的 25%。在云环境中,安装了 JDK 的容器映像经常通过网络从容器注册表中复制,因此减少 JDK 大小可以提高效率。

$ configure [ ... other options ... ] --enable-linkable-runtime $ make images

然后,创建仅包含 java.xml 和 java.base 模块的运行时映像:

$ jlink --add-modules java.xml --output image $ image/bin/java --list-modules java.base@24 java.xml@24

在此示例中,jlink 仅使用选定的模块创建运行时映像,而不需要 JMOD 文件,从而生成较小的最终映像。

Nihil Novi Sub Sole - 预览功能

也就是说,这些是尚未收到重大更新的 JEP。供参考:

JEP 487:范围值(第四个预览版)
JEP 487 的目标是引入 Scoped Values,这是一种虚拟线程友好的 API 替代方案ThreadLocal。它们允许方法与同一线程及其子线程中的直接和间接调用共享不可变数据。与线程局部变量相比,Scoped Values 更容易理解,并且内存和时间开销更低,尤其是与虚拟线程和结构化并发结合使用时。

问题解决了吗?

在传统的 Java 编程中,数据通过参数在方法之间传递。然而,在控制流在不同组件之间的复杂应用程序中,通过连续方法传递所有必要的数据变得不切实际。迄今为止使用的线程局部变量可以在线程内共享数据,但存在管理困难和潜在的性能问题。

解决方案

范围值可以定义不可变数据,这些数据可以在同一线程及其子线程中的方法调用范围内共享。这种方法允许更清晰、更高效地传递数据,而无需修改方法签名。与线程局部变量相比,范围值提供了更好的代码可读性和更低的运营成本,尤其是在虚拟线程和结构化并发的环境中。

代码示例

public class ScopedValueExample {
    private static final ScopedValue<String> USER_ID = ScopedValue.newInstance();

    public static void main(String[] args) {
        ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

        ScopedValue.where(USER_ID, "user123").run(() -> {
            System.out.println(
"User ID: " + USER_ID.get()); // Output: User ID: user123

            executor.submit(() -> {
                System.out.println(
"User ID in child thread: " + USER_ID.get()); // Output: User ID in child thread: user123
            });
        });

        executor.shutdown();
    }
}

这里,USER_ID范围值设置为“user123”,并在当前线程及其子线程中共享,而不需要将其作为参数传递。

JEP 历史和先前的迭代:Scoped Values 最初作为预览功能在 JDK 20(JEP 429)中引入。后续预览版出现在 JDK 21(JEP 436)和 JDK 22(JEP 452)中。JEP 487 中的第四个预览版侧重于收集更多用户反馈,并确保 API 在最终确定之前符合实际应用需求。

JEP 488:模式中的原始类型、instanceof 和 switch(第二个预览)
JEP 488 的目标是扩展 Java 中的模式匹配,以支持所有模式上下文中的原始类型,并允许对这些类型使用instanceof和切换运算符。此功能目前作为预览功能提供。

解决的问题:

以前,switch 中的模式匹配不支持原始类型,这限制了灵活性和代码的可读性。例如,处理基于整数的状态代码需要使用带有附加操作的默认情况,而不是直接匹配特定值。此外,记录模式不支持原始类型,这增加了记录中数据的处理和分解的复杂性。

解决方案:

JEP 488 允许在模式匹配中使用原始类型。此增强功能使 switch 语句能够直接处理原始值,从而简化代码并提高可读性。原始模式还可以嵌套在更复杂的模式中,例如在记录中,从而促进数据结构的自然分解。

代码示例:

switch (x.getStatus()) {
    case 0 -> "okay";
    case 1 ->
"warning";
    case 2 ->
"error";
    case int i ->
"unknown status: " + i;
}

这里,状态被匹配为一个整数,从而无需使用默认值来处理未知值。

另一个(记录)示例:

sealed interface JsonValue {
    record JsonString(String s) implements JsonValue { }
    record JsonNumber(double d) implements JsonValue { }
    record JsonObject(Map<String, JsonValue> map) implements JsonValue { }
}

var json = new JsonObject(Map.of("name", new JsonString("John"),
                                 
"age",  new JsonNumber(30)));

if (json instanceof JsonObject(var map)
    && map.get(
"name") instanceof JsonString(String n)
    && map.get(
"age")  instanceof JsonNumber(int a)) {
    System.out.println(
"Name: " + n + ", Age: " + a);
}

通过 JEP 488 中的增强,JsonNumber可以直接匹配为 int,简化数据处理并提高可读性。

JEP 489:Vector API(第九个孵化器)
JEP 489 的目标是引入一个 API,使向量计算能够用 Java 表达,并在运行时可靠地编译为受支持的 CPU 架构上的最佳向量指令。这可以实现超越等效标量计算的性能。

它解决的问题:

向量计算可以同时处理多个数据元素,这对于多媒体处理和机器学习算法等高性能应用至关重要。Java 中缺乏标准 API,这阻碍了开发人员有效利用当前处理器上可用的现代向量指令。因此,与使用可直接访问此类指令的语言编写的应用程序相比,Java 应用程序可能无法实现最佳性能。

建议的解决方案:

Vector API 的引入使 Java 开发人员能够以独立于处理器架构的方式定义矢量运算。此 API 可确保矢量计算在运行时编译为平台上可用的适当矢量指令,例如 x64 上的 SSE 或 AVX 以及 ARM AArch64 上的 NEON。这使应用程序能够在不损失代码可移植性的情况下实现更高的性能。

代码示例:

  public static void main(String[] args) {
        VectorSpecies<Float> SPECIES = FloatVector.SPECIES_256;
        float[] a = {1.0f, 2.0f, 3.0f, 4.0f};
        float[] b = {5.0f, 6.0f, 7.0f, 8.0f};
        float[] c = new float[4];

        
        VectorMask<Float> mask = SPECIES.indexInRange(0, a.length);

        FloatVector va = FloatVector.fromArray(SPECIES, a, 0, mask);
        FloatVector vb = FloatVector.fromArray(SPECIES, b, 0, mask);

        FloatVector vc = va.add(vb);

        vc.intoArray(c, 0, mask);

        for (float f : c) {
            System.out.println(f);
        }
    }

在上面的例子中,Vector API 用于以矢量化的方式添加两个浮点数数组,从而能够使用现代处理器指令进行高效的数据处理。

历史和以前的迭代:仍在等待 Valhalla 项目。


JEP 492:灵活的构造函数主体(第三次预览)
JEP 492 的目标是在调用另一个构造函数之前将语句放在 Java 构造函数中,即super(..) 或this(..)。这些语句不能引用构造的实例,但可以初始化其字段。在调用另一个构造函数之前初始化字段可以提高类的可靠性,尤其是在方法被重写时。

它解决的问题:

super(..)以前,构造函数中的第一个语句必须是对另一个构造函数(或)的调用this(..)。此限制限制了在调用超类构造函数之前执行参数验证或字段初始化等操作的能力。因此,开发人员必须使用辅助静态方法等变通方法,这会使代码变得复杂,并且更难维护。

建议的解决方案:

灵活的构造函数主体允许在调用另一个构造函数之前放置语句。这些语句形成一个“序言”,在调用超类构造函数之前启用字段初始化或参数验证。但是,序言不能引用构造的实例。调用超类构造函数后,将执行包含其余语句的“尾声”。这种方法通过消除对其他辅助方法的需求来提高代码的可读性和可靠性。

代码示例:

public class PositiveBigInteger extends BigInteger {

    public PositiveBigInteger(long value) {
        if (value <= 0) throw new IllegalArgumentException("Value must be positive");
        super(value);
    }
}

在上面的例子中,在调用超类构造函数 BigInteger 之前会执行参数值的验证。如果值无效,则会提前抛出异常,从而防止不必要的对象创建。

历史和以前的迭代:JEP 492 最初是在 JDK 23 中作为预览功能提出的。这是 JDK 24 中引入的第二个预览版本,保留了原始功能。在此迭代中,文档得到了改进,实现细节也得到了更新,但功能本身保持不变。

JEP 494:模块导入声明(第二预览版)
JEP 494 的目标是引入一种简洁的方法来导入 Java 中模块导出的所有包。这简化了模块库的重用,而无需为每个包进行显式导入声明。

它解决的问题

目前,开发人员必须在每个源文件的开头添加大量导入声明,这增加了代码的复杂性并降低了可读性。虽然 Java 9 中引入的模块允许将包分组到共享名称下,但没有机制可以简洁地导入模块导出的所有包。

建议的解决方案

JEP 494 引入了 import module M; 形式的模块导入声明,允许按需导入模块 M 及其传递依赖项导出的包中的所有公共类和接口。这使开发人员能够使用单个模块导入声明访问广泛的类和接口,从而简化代码并提高可读性。例如,import module java.base; 提供对模块导出的所有包的访问java.base,例如java.util和java.nio.file。

代码示例:

import module java.base;

public class Example {
    public static void main(String[] args) {
        List<String> list = List.of("apple", "banana", "cherry");
        Path path = Path.of(
"/example/path");
       
// ...
    }
}

在上面的例子中,导入模块 java.base; 声明提供了对 java.util 包中的 List 类和包中的 Path 类的访问,java.nio.file而无需单独导入包。

历史和先前的迭代:模块导入声明最初是在 JEP 476 (JDK 23) 中作为预览功能提出的。第二次迭代消除了阻止模块声明对 java.base 模块的传递依赖的限制,并更新了java.se模块声明以传递地需要 java.base。通过这些更改,导入 java.se 模块现在可以根据需要导入整个 Java SE API。

此外,现在允许按需类型导入声明覆盖模块导入声明。

JEP 494:模块导入声明(第二预览版)
JEP 494 的目标是引入简洁地导入 Java 语言中模块导出的所有包的功能。这有助于重复使用模块库,而无需在每个模块中都进行导入声明。

解决的问题:

目前,开发人员必须在每个源文件的开头添加大量导入声明,这增加了代码的复杂性并降低了可读性。此外,虽然 Java 9 中引入了模块,允许将包分组到共享名称下,但没有简洁地导入模块导出的所有包的机制。

解决方案:

JEP 494 引入了 import module M; 形式的模块导入声明,允许按需导入模块 M 导出的包中的所有公共类和接口,并可由 M 所依赖的模块传递导入。这使开发人员能够使用单个模块导入声明访问各种类和接口,从而简化代码并提高可读性。例如, import module java.base; 授予对 java.base 模块导出的所有包的访问权限,例如java.util和java.nio.file。

代码示例:

import module java.base;

public class Example {
    public static void main(String[] args) {
        List<String> list = List.of("apple", "banana", "cherry");
        Path path = Path.of(
"/example/path");
       
// ...
    }
}

在上面的例子中,导入模块 java.base; 声明提供了对 java.util 包中的 List 类和Pathjava.nio.file 包中的类的访问,而不需要单独导入这些包。

模块导入声明的引入通过实现更简洁、更易读的依赖关系管理简化了 Java 编程。

JEP 历史和先前的迭代:模块导入声明最初是在 JEP 476 (JDK 23) 中作为预览功能提出的。在第二次迭代中,引入了两项更改:取消了阻止模块声明对 java.base 模块的传递依赖的限制,并且更新了 java.se 模块声明以传递需要 java.base 模块。通过这些更改,导入 java.se 模块将根据需要包含整个 Java SE API。

此外,类型导入按需声明可以覆盖模块导入声明。

JEP 495:简单源文件和实例主要方法(第四个预览版)
JEP 495 简化了用 Java 编写第一个程序的过程,允许创建简单的源文件而无需类定义,并允许将主要方法定义为实例方法,而无需使用静态修饰符。这使得 Java 代码更加简洁易读,尤其是对于初学者来说,简化了学习过程。

解决的问题:传统上,即使是最简单的 Java 程序也需要定义一个类和一个静态 main 方法,这给初学者增加了不必要的复杂性。例如,“Hello, World!”程序必须写成:

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

这种结构要求初学者理解类、访问修饰符和数组参数等概念,这在学习的早期阶段可能会让人难以理解。

解决方案:

JEP 495 允许创建简单的源文件,其中可以直接定义 main 方法,而无需封闭类。此外,控制台输入和输出的有用方法会自动导入到这些简单文件中,从而可以使用更紧凑的形式。

对于需要数据结构或 I/O 操作的更高级程序,java.lang 包之外的标准 API 会自动在简单的源文件中导入。

代码示例:

void main() {
    var list = List.of("apple", "banana", "cherry");
    for (var fruit : list) {
        println(fruit);
    }
}

在上面的例子中,由于自动导入,可以使用 List 类,而无需手动导入 java.util 包。

JEP 历史和先前的迭代:JEP 495 中描述的功能之前已在 JEP 445 (JDK 21)、JEP 463 (JDK 22) 和 JEP 477 (JDK 23) 的预览版本中提出。在每次迭代中,都会根据用户体验和反馈对功能进行改进和调整。在此第四个预览版本中,引入了新术语,并更新了标题(再次;还记得隐式类吗?),但功能本身保持不变,以便进一步获得用户反馈和改进。

JEP 499:结构化并发(第四个预览版)
JEP 499 通过引入结构化并发 API 简化了并发编程。结构化并发将在不同线程中运行的相关任务组视为单个工作单元,从而简化了错误处理、任务取消、可靠性和可观察性。

解决的问题:

Java 中传统的并发编程方法基于 ExecutorService 和 Future,允许并行执行任务,但不提供父任务和子任务之间关系的结构。这种结构缺失可能会导致线程泄漏、任务取消不当或难以诊断错误等问题。例如,如果多个并行任务中的一个失败,其他任务可能会继续运行,从而导致不良的副作用。

解决方案:

JEP 499 引入了StructuredTaskScope,它允许将相关任务分组为单个工作单元。这允许并行运行多个任务并以有序的方式等待。如果一个任务失败,则StructuredTaskScope自动取消其他任务,从而防止线程泄漏并简化错误处理。

此外,结构化并发提高了并发代码的可观察性,从而能够更好地监控和诊断问题。

代码示例:

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future<String> user  = scope.fork(() -> findUser());
    Future<Integer> order = scope.fork(() -> fetchOrder());
    scope.join();           // Waits for all tasks to complete
    scope.throwIfFailed();  
// Throws an exception if any task failed
    String theUser  = user.resultNow();
    int    theOrder = order.resultNow();
    return new Response(theUser, theOrder);
}

在上面的示例中,StructuredTaskScope.ShutdownOnFailure 创建了一个范围,其中两个任务分别在 和parallel: findUser()中运行fetchOrder()。该scope.join()方法等待两个任务完成,scope.throwIfFailed()如果任何任务失败,则抛出异常。

这种错误处理和任务取消的集成方法简化了代码并提高了其可靠性。

JEP 历史和先前的迭代:

结构化并发最初是在 JEP 428 中提出的,并在 JDK 19 中作为孵化 API 引入。然后,它在 JDK 20 中由 JEP 437 重新孵化,并进行了与继承范围值相关的小更新。第一个预览出现在 JDK 21 中,直到 JEP 453,其中方法StructuredTaskScope::fork更改为返回 Subtask 而不是 Future。后续预览包含在 JDK 22(JEP 462)和 JDK 23(JEP 480)中,没有重大变化。JEP 499 现在在 JDK 24 中提出了第四个预览,同样没有重大更新。

各位,这就是JDK 24 的全部 24 个新 JEP !