2024年3月19日,Oracle 官网正式发布了 JDK22,虽然这是一个非 LTS(长期支持)版本,但 JDK22也带来了一些引人注目的新特性,V哥详细阅读了官网文档,把相关新特性总结出来,分享给大家,一睹为快。
这是官网对 JDK22版本新特性的概要截图:
翻译一下:
::: block-1
JDK 22 是 Java SE 平台的第 22 个版本的参考实现,它遵循 Java 社区进程中的 JSR 397 规范。在 2024 年 3 月 19 日,JDK 22 达到了通用可用性阶段,意味着它已经准备好用于生产环境。Oracle 提供了符合 GPL 协议的生产就绪二进制文件,而其他供应商的二进制文件也将很快跟进。
该版本的特性和计划通过 JEP(JDK 增强提案)流程提出和跟踪,根据 JEP 2.0 提案进行了修订。本次发布使用 JDK 发布流程(JEP 3)进行。
以下是 JDK 22 中的一些主要特性:
关于 JDK 22 的发布计划:
:::
下面跟V哥一起来针对每个新特性做详细的解释:
423: G1 的区域引入
核心要点
:JEP 423 引入了 G1 垃圾回收器中的区域固定(Region Pinning)功能,这是为了减少 Java 原生接口(JNI)关键区域期间的延迟。通过这个特性,垃圾回收在 JNI 关键区域期间不需要被禁用,从而提高了与本地代码交互时的性能。
示例代码
:由于 JEP 423 主要是关于垃圾回收器的内部工作机制,它并不直接提供新的API或者对开发者可见的行为改变,因此没有直接的示例代码。不过,我们可以展示一个使用 JNI 的场景,来说明在没有 JEP 423 之前可能遇到的问题,以及 JEP 423 如何改善这种情况。
public class NativeObject { // 假设这是一个通过 JNI 创建的本地对象 private long nativeObject; // 使用 JNI 创建本地对象 public NativeObject() { // 调用本地方法创建对象,这将进入 JNI 关键区域 nativeObject = createNativeObject(); } // 释放本地对象,也需要通过 JNI public void release() { // 调用本地方法释放对象,这同样是一个 JNI 关键区域 releaseNativeObject(nativeObject); nativeObject = 0; // 清除引用 } // 调用本地方法创建本地对象的静态方法 private native long createNativeObject(); // 调用本地方法释放本地对象的静态方法 private native void releaseNativeObject(long object);}public class Main { public static void main(String[] args) { NativeObject obj = null; try { obj = new NativeObject(); // 在这里可以对 obj 进行操作 } finally { if (obj != null) { obj.release(); // 释放本地对象资源 } } }}
在这个示例中,NativeObject 类使用 JNI 创建和释放本地资源。在没有 JEP 423 之前,每次进入 JNI 关键区域,G1 垃圾回收器都会暂停,等待关键区域结束。这可能导致性能问题,尤其是在 JNI 关键区域频繁且持续时间较长的情况下。
JEP 423 通过在 G1 垃圾回收器中实现区域固定,允许在 JNI 关键区域期间继续进行垃圾回收,从而减少了延迟。这样,即使在频繁使用 JNI 的应用程序中,也能保持更好的响应性和吞吐量。
447: 在 super(…) 之前的语句(预览版)
核心要点
:JEP 447 引入了在构造函数中超类构造函数调用之前的语句的预览特性。这个特性允许开发者在显式调用超类构造函数之前执行一些不引用正在创建的实例的语句。这提供了更大的灵活性,使得构造函数中的逻辑可以更自然地放置,而不必依赖于辅助静态方法、中间构造函数或构造函数参数。
示例代码
:
public class ValidatedBigInteger extends BigInteger { // 构造函数中使用预览特性,在调用超类构造函数之前执行验证 public ValidatedBigInteger(long value) { if (value <= 0) { // 执行验证逻辑 throw new IllegalArgumentException("非正整数"); } super(value); // 调用超类构造函数 }}
在这个示例中,ValidatedBigInteger 类在创建实例之前先验证了传入的 value 是否大于0。如果 value 不大于0,它将抛出一个 IllegalArgumentException。然后,如果验证通过,它将调用超类 BigInteger 的构造函数来完成实例的创建。在JEP 447之前,这样的逻辑需要使用一个辅助方法来完成,而这个特性使得可以直接在构造函数中包含这种验证逻辑,使得代码更加清晰和直观。
454: 外来函数与内存 API
核心要点
:JEP 454引入了Foreign Function & Memory API (FFM API),它允许Java程序高效且安全地调用本地(外部)函数和访问外部内存,从而替代了JNI的一部分功能,并提供了更好的性能和更简洁的API。
示例代码
:以下是一个使用FFM API调用C库中的strlen函数来计算字符串长度的示例。
// 获取链接器和符号查找器Linker linker = Linker.nativeLinker();SymbolLookup stdlib = linker.defaultLookup();// 找到C库中的strlen函数MemorySegment strlenSymbol = stdlib.find("strlen").get();// 创建一个下调用方法句柄,它将strlen函数作为本地方法调用MethodHandle strlenMH = linker.downcallHandle(strlenSymbol, FunctionDescriptor.of(JAVA_LONG, ADDRESS));// 分配一个内存段来存储一个字符串MemorySegment strSegment = Arena.allocateFrom("Hello, World!");// 调用strlen函数来获取字符串的长度long stringLength = (long) strlenMH.invokeExact(strSegment);// 输出字符串长度System.out.println("String length: " + stringLength);
在这个示例中,我们首先通过Linker获取到C标准库中的strlen函数的内存段引用,然后创建一个方法句柄来调用这个函数。接着,我们分配了一个内存段来存储一个字符串,并使用strlenMH方法句柄来调用strlen函数,获取并打印字符串的长度。这个过程中,我们不需要使用JNI,而是直接通过Java代码来完成对本地函数的调用和内存的管理。
456: 未命名变量与模式
核心要点
:
示例代码
:
// 使用未命名变量的增强 for 循环static int count(Iterable<Order> orders) { int total = 0; for (Order _ : orders) { // 使用未命名变量 total++; } return total;}// 在 while 循环中使用未命名变量Queue<Integer> q = ...;while (q.size() >= 3) { var x = q.remove(); var _ = q.remove(); // 使用未命名变量 var _ = q.remove(); // 使用未命名变量 ... new Point(x, 0) ... // 只使用 x}// 使用未命名变量的 catch 块String s = ...;try { int i = Integer.parseInt(s); ... i ...} catch (NumberFormatException _ ) { // 使用未命名变量 System.out.println("Bad number: " + s);}// 在 try-with-resources 语句中使用未命名变量try (var _ = ScopedContext.acquire()) { // 使用未命名变量 ... // 不使用 acquired resource}// 使用未命名变量的 lambda 表达式...stream.collect(Collectors.toMap(String::toUpperCase, _ -> "NODATA")) // 使用未命名变量// 使用未命名模式变量的 switch 语句switch (ball) { case RedBall _ -> process(ball); case BlueBall _ -> process(ball); case GreenBall _ -> stopProcessing();}// 使用未命名模式的 instanceof 检查if (r instanceof ColoredPoint(Point(int x, _), _)) { // 未使用的组件使用未命名模式 ... x ...}
这些示例展示了如何在不同场景下使用未命名变量和未命名模式,从而简化代码并提高可读性。未命名变量和模式的使用消除了对未使用变量的命名需求,使得代码更加简洁和直观。
457: 类文件 API(预览版)
核心要点
:
示例代码
:
由于JEP 457是一个预览API,且涉及到的类文件操作通常不是在普通的Java应用程序中直接编写的,因此这里提供的示例代码是概念性的,用于说明如何使用API进行类文件的处理。
// 解析类文件ClassFile classFile = ClassFile.of();ClassModel classModel = classFile.parse(bytes);// 生成类文件byte[] newBytes = classFile.build(classModel.thisClass().asSymbol(), classBuilder -> { // 使用构建器添加方法、字段等 classBuilder.withMethod("newMethod", MethodTypeDesc.of(void.class), methodBuilder -> { // 构建方法体 methodBuilder.withCode(codeBuilder -> { // 添加字节码指令 codeBuilder.aload(0).invokevirtual(MethodDesc.of(Object.class, "toString", String.class)); }); }); });// 转换类文件byte[] transformedBytes = classFile.transform(classModel, (classBuilder, ce) -> { if (ce instanceof MethodModel && ce.methodName().stringValue().startsWith("debug")) { // 忽略以"debug"开头的方法 } else { // 否则保留原始元素 classBuilder.with(ce); }});
请注意,这些代码片段是为了展示API的用法而设计的,并不是完整的工作代码。在实际使用中,你需要根据具体的API文档和可用的类文件操作功能来编写代码。此外,由于这是一个预览API,它可能在未来的Java版本中会有所改变。在使用之前,需要确保你的开发环境已经启用了预览特性。
458: 启动多文件源代码程序
JEP 458的目标是增强Java应用程序启动器(java launcher),使其能够运行由多个Java源代码文件组成的程序。这将使得从小型程序过渡到大型程序的过程更加渐进,允许开发者自行决定何时配置构建工具。以下是JEP 458的核心要点和示例代码:
核心要点
:
多文件源代码程序支持:开发者可以使用java命令直接运行由多个.java文件组成的程序,而不需要先将它们编译成.class文件。
源文件模式:通过传递单个.java文件的名称来触发启动器的源文件模式。如果提供了额外的文件名,它们将成为主方法的参数。
类路径和模块路径:程序可以从源文件启动,并且依赖于类路径或模块路径上的库。
目录结构:源文件需要按照包结构组织在目录中。例如,包名为pkg的类应该放在foo/bar目录下。
启动时编译:启动器在运行时编译源文件,可能会根据需要逐步编译或延迟编译。
启动类选择:启动器会选择具有标准main方法的类作为启动类。如果.java文件中的第一个顶级类声明了main方法,则该类为启动类。如果其他顶级类声明了与文件同名的标准main方法,则该类为启动类。
内存中的编译:编译的类存储在内存缓存中,而不是写入磁盘。
反射和注解处理:通过反射访问的类和直接访问的类以相同的方式加载。注解处理在源文件模式下被禁用。
限制:源文件程序不能跨越多个模块,这可能是未来的改进点。
示例代码
:
假设我们有两个源文件Prog.java和Helper.java,它们位于同一个目录中:
// Prog.javaclass Prog { public static void main(String[] args) { Helper.run(); }}// Helper.javaclass Helper { static void run() { System.out.println("Hello, World!"); }}
要运行这个程序,只需在命令行中使用java命令:
$ java Prog.java
启动器会自动找到Helper.java文件,编译它,然后执行Prog类的main方法。这个过程不需要开发者手动编译任何.java文件,使得从编辑到运行的周期更加简洁。
459: 字符串模板(第二次预览版)
JEP 459 引入了字符串模板(String Templates)作为 Java 语言的第二个预览功能,旨在提供一种更清晰、更安全的方式来构建包含运行时计算值的字符串。
核心要点
:
字符串模板:一种新的表达式,允许在字符串中嵌入表达式,并在运行时替换这些表达式。
模板处理器:用于处理字符串模板并生成结果。STR 和 FMT 是 Java 平台提供的内置模板处理器,分别用于基本的字符串插值和格式化。
安全性:字符串模板的设计确保了在将表达式的值插入到字符串中时的安全性,防止了注入攻击等安全问题。
国际化和本地化:字符串模板可以简化国际化和本地化的过程,通过模板处理器来处理不同的语言和格式。
API 支持:提供了 StringTemplate 接口和相关的 API,允许开发者创建自定义的模板处理器。
多行模板:字符串模板可以跨越多行,类似于文本块(text block)的语法。
示例代码
:
// 使用 STR 模板处理器进行基本的字符串插值String name = "Joan";String info = STR.My name is \{name};System.out.println(info); // 输出:My name is Joan// 使用 FMT 模板处理器进行格式化double value = 123.456;String formatted = FMT."The value is %6.2f";System.out.println(formatted); // 输出:The value is 123.46// 多行模板示例String html = STR.""" <html> <head> <title>My Web Page</title> </head> <body> <p>Hello, world!</p> </body> </html>""";System.out.println(html);// 自定义模板处理器示例var INTER = StringTemplate.Processor.of((StringTemplate st) -> { StringBuilder sb = new StringBuilder(); Iterator<String> fragIter = st.fragments().iterator(); for (Object value : st.values()) { sb.append(fragIter.next()); sb.append(value); } sb.append(fragIter.next()); return sb.toString();});String result = INTER."\{value\}";System.out.println(result); // 输出:{value}
这些示例展示了如何使用内置的 STR 和 FMT 模板处理器,以及如何创建自定义的模板处理器。字符串模板提供了一种灵活且表达性强的方式来构建和管理字符串,特别是在需要动态插入变量值时。
460: 向量 API(第七个孵化版)
JEP 460 旨在将 Vector API 作为第七个孵化版引入 JDK 22。这个 API 允许开发者编写能够在支持的 CPU 架构上编译为高效的硬件向量指令的向量计算代码。
核心要点
:
API 目标:提供一个清晰、简洁的 API,用于表达各种向量计算,包括循环内的序列化向量操作和可能的控制流。API 应该能够表达与向量大小(每个向量中的通道数)无关的通用计算,从而在支持不同向量大小的硬件上实现可移植性。
平台无关性:API 应该是 CPU 架构无关的,支持在多种支持向量指令的架构上实现。如果平台优化和可移植性之间存在冲突,API 将倾向于可移植性。
在 x64 和 AArch64 架构上的可靠运行时编译和性能:在支持的 x64 架构上,Java 运行时(特别是 HotSpot C2 编译器)应该能够将向量操作编译为高效的向量指令,如 SSE 和 AVX 支持的指令。对于 ARM AArch64 架构,C2 也应该将向量操作编译为 NEON 和 SVE 支持的向量指令。
优雅降级:如果向量计算不能完全表达为向量指令序列,例如因为架构不支持某些必需的指令,Vector API 的实现应该能够优雅降级并继续运行。这可能涉及在无法有效编译为向量指令的向量计算时发出警告。
与 Project Valhalla 保持一致:Vector API 的长期目标是利用 Project Valhalla 对 Java 对象模型的增强。这意味着将 Vector API 中的当前基于值的类更改为值类,以便程序可以使用值对象,即缺乏对象身份的类实例。
非目标:不旨在增强 HotSpot 中现有的自动向量化算法;不支持除 x64 和 AArch64 之外的 CPU 架构的向量指令;不支持 C1 编译器;不保证支持严格浮点计算。
示例代码
:
以下是一个简单的示例,展示了如何使用 Vector API 来执行浮点数数组的向量计算:
import jdk.incubator.vector.FloatVector;import jdk.incubator.vector.VectorSpecies;public class VectorExample { static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED; public static void main(String[] args) { float[] arrayA = { /* ... 初始化数组 ... */ }; float[] arrayB = { /* ... 初始化数组 ... */ }; float[] resultArray = new float[arrayA.length]; int i = 0; int upperBound = SPECIES.loopBound(arrayA.length); for (; i < upperBound; i += SPECIES.length()) { FloatVector va = FloatVector.fromArray(SPECIES, arrayA, i); FloatVector vb = FloatVector.fromArray(SPECIES, arrayB, i); FloatVector vc = va.mul(va).add(vb.mul(vb)).neg(); vc.intoArray(resultArray, i); } for (; i < arrayA.length; i++) { resultArray[i] = (arrayA[i] * arrayA[i] + arrayB[i] * arrayB[i]) * -1.0f; } }}
在这个示例中,我们首先获取一个最优的向量种类(SPECIES),然后在一个循环中使用向量操作来计算两个数组的元素的特定数学表达式,并将结果存储到另一个数组中。如果在数组的末尾有剩余的元素,我们使用一个普通的标量循环来处理这些元素。这个例子展示了如何使用 Vector API 来进行向量化计算,以期在支持的硬件上获得更好的性能。
461: 流收集器(预览版)
JEP 461 引入了流收集器(Stream Gatherers)作为 Java 流(Stream)API 的预览特性,允许开发者自定义中间操作,从而增强了流管道的灵活性和表达能力。这项增强功能旨在让开发者能够以各种方式转换有限和无限的流,这些方式使用现有的内置中间操作难以实现。
核心要点
:
自定义中间操作:定义自定义操作的能力,可以按照一对一、一对多、多对一或多对多的方式转换流中的元素。
收集器接口:收集器由四个函数定义:一个可选的初始化器、一个集成器、一个可选的合并器和一个可选的完成器。这些函数共同作用于处理流元素并维护状态。
支持无限流:自定义中间操作可以处理无限大小的流,并且有可能将它们转换为有限的流,通过短路机制实现。
并行执行:当提供了合并器函数时,收集器可以支持并行执行,允许在并行流中高效处理。
内置收集器:java.util.stream.Gatherers 类引入了几个内置收集器,如 fold、mapConcurrent、scan、windowFixed 和 windowSliding。
组合收集器:收集器可以使用 andThen 方法进行组合,通过结合更简单的收集器来创建复杂的收集器。
收集器与收集器的区别:虽然收集器类似于收集器,但它们在处理每个元素时使用集成器(而不是双消费者),并且在完成器中使用双消费者(而不是函数),因为它们需要处理下游对象,并且不能返回结果。
示例代码
:
以下是使用内置收集器 windowSliding 将元素分组到滑动窗口中的示例:
import java.util.List;import java.util.stream.Collectors;import java.util.stream.Stream;public class StreamGathererExample { public static void main(String[] args) { Stream<String> stream = Stream.of("a", "b", "c", "d", "e"); // 将元素分组到大小为2的滑动窗口中 List<List<String>> slidingWindows = stream.gather(Gatherers.windowSliding(2)); System.out.println(slidingWindows); // 输出:[[a, b], [c, d], [e]] }}
以下是定义一个固定大小窗口的自定义收集器的示例:
import java.util.ArrayList;import java.util.List;import java.util.function.BinaryOperator;import java.util.stream.Gatherer;public class AdHocGathererExample { public static <T> Gatherer<T, ArrayList<T>, List<T>> fixedWindow(int windowSize) { return Gatherer.of( () -> new ArrayList<>(windowSize), // 初始化器 (window, element, downstream) -> { window.add(element); return window.size() < windowSize; // 集成器 }, BinaryOperator.minBy((a, b) -> a.size() - b.size()), // 合并器(用于并行) (window, downstream) -> { if (!window.isEmpty()) { downstream.push(new ArrayList<>(window)); } } // 完成器 ); } public static void main(String[] args) { Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9); // 将元素分组到大小为3的固定窗口中 List<List<Integer>> fixedWindows = stream.gather(fixedWindow(3)).toList(); System.out.println(fixedWindows); // 输出:[[1, 2, 3], [4, 5, 6], [7, 8, 9]] }}
这些示例展示了新的流收集器特性的灵活性和强大能力,允许开发者创建以前使用标准流 API 无法实现的自定义流转换。
462: 结构化并发(第二次预览版)
JEP 462 旨在通过引入结构化并发的 API 来简化并发编程。以下是其核心要点和示例代码:
核心要点
:
示例代码
:
以下是一个使用 StructuredTaskScope 的简单示例,它演示了如何在服务器应用程序中处理两个并发的子任务:
import java.util.concurrent.CompletableFuture;import java.util.concurrent.StructuredTaskScope;import java.util.concurrent.TimeUnit;import java.util.stream.Stream;public class StructuredConcurrencyExample { public static void main(String[] args) { try (StructuredTaskScope<String> scope = new StructuredTaskScope<>()) { // 分叉两个子任务 Supplier<String> task1 = scope.fork(() -> findUser()); Supplier<String> task2 = scope.fork(() -> fetchOrder()); // 等待所有子任务完成或被取消 scope.join(); // 检查是否有子任务失败,并抛出异常 scope.throwIfFailed(); // 处理子任务的结果 String user = task1.get(); String order = task2.get(); System.out.println("User: " + user + ", Order: " + order); } catch (Exception e) { e.printStackTrace(); } } private static String findUser() { // 模拟用户查找逻辑 return "User123"; } private static String fetchOrder() { // 模拟订单获取逻辑 return "Order456"; }}
在这个示例中,我们创建了一个 StructuredTaskScope 实例,并在其中分叉了两个子任务 task1 和 task2。我们使用 fork 方法来启动这些子任务,并在 join 方法中等待它们完成。如果所有子任务都成功完成,我们可以通过调用 get 方法来获取结果。如果任何子任务失败,throwIfFailed 方法将抛出异常,允许我们处理错误。这个结构化的方法提供了一种清晰的方式来管理并发任务的生命周期和错误处理。
463: 隐式声明的类和实例主方法(第二次预览版)
JEP 463 旨在改进 Java 语言,使其对初学者更加友好,允许编写不依赖于复杂语言特性的基本程序。这是继 JEP 445 之后的第二轮预览功能,提供了一些重要的变化,并因此更新了标题。
核心要点
:
示例代码
:
以下是一个使用 JEP 463 功能的简单示例,展示了如何编写一个没有显式类声明的 main 方法:
// 这是一个简单的 Hello, World! 程序,没有显式的类声明
void main() { System.out.println("Hello, World!");}
如果你想要在同一个文件中包含多个方法,可以这样做:
// 使用隐式声明的类,包含多个方法String greeting() { return "Hello, World!";}void main() { System.out.println(greeting());}
或者,你可以使用一个字段:
// 使用隐式声明的类,包含一个字段String greeting = "Hello, World!";void main() { System.out.println(greeting);}
要尝试这些示例,你需要在 JDK 22 中启用预览功能,如下所示:
javac --release 22 --enable-preview Main.javajava --enable-preview Main
或者,如果你使用的是源代码启动器:
java --source 22 --enable-preview Main.java
JEP 463 的目标是提供一个平滑的学习曲线,使得学生和新程序员能够以简洁的方式编写基本程序,并随着他们技能的提升,逐步扩展到使用 Java 的更高级特性。这个特性不会引入 Java 语言的另一个初学者方言,也不会引入一个单独的初学者工具链。
464: 作用域值(第二次预览版)
JEP 464 引入了作用域值(Scoped Values),这是一种新的机制,用于在同一个线程及其子线程之间共享不可变数据。作用域值旨在解决线程局部变量(ThreadLocal)的一些设计缺陷,提供更简单、更安全且性能更优的数据共享方式。
核心要点
:
示例代码
:
以下是一个使用 JEP 464 的作用域值的示例:
import java.util.concurrent.ScopedValue;// 定义一个作用域值,用于存储用户信息private static final ScopedValue<UserInfo> currentUser = ScopedValue.newInstance();// 模拟用户信息的创建UserInfo createUserInfo(String username) { // 创建并返回用户信息对象 return new UserInfo(username);}// 模拟一个需要用户信息的方法void processRequest(String username) { // 绑定当前请求的用户信息 ScopedValue.where(currentUser, createUserInfo(username)).run(() -> { // 在这个作用域内,可以直接获取当前用户信息 UserInfo userInfo = currentUser.get(); // 处理请求... });}// 另一个方法,可能在另一个线程中执行void performTask() { // 在这个线程中,我们可以安全地读取用户信息 UserInfo userInfo = currentUser.get(); // 执行任务...}
在这个示例中,我们创建了一个名为 currentUser 的 ScopedValue 对象,用于存储当前用户的信息。在 processRequest 方法中,我们通过调用 ScopedValue.where 方法来绑定用户信息,并在一个特定的作用域内执行代码。在 performTask 方法中,我们可以在另一个线程中安全地读取相同的用户信息,因为作用域值被子线程继承。
请注意,为了使用作用域值,你需要启用预览功能。在 JDK 22 中,你可以通过以下方式编译和运行使用作用域值的程序:
javac --release 22 --enable-preview YourProgram.javajava --enable-preview YourProgram
JEP 464 的作用域值提供了一种新的、改进的数据共享机制,特别适用于多线程和并发编程场景。
最后
以上是介绍 JDK22新特性的全部内容了,突然V哥想要感慨一下,技术之路,学无止境,选择 IT 技术,作个纯粹的人,享受研究技术的过程,这种带来的快感,也许只有真正热爱编程的人才能有体会。