2024年3月19日,Oracle 官网正式发布了 JDK22,虽然这是一个非 LTS(长期支持)版本,但 JDK22也带来了一些引人注目的新特性,V哥详细阅读了官网文档,把相关新特性总结出来,分享给大家,一睹为快。
这是官网对 JDK22版本新特性的概要截图:
翻译一下:
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 中的一些主要特性:
- 423: G1 的区域引入:通过引入“区域锁定”来减少 G1 垃圾回收器的延迟,确保在 JNI 的关键区域无需关闭垃圾回收进程。
- 447: 在 super(...) 之前的语句(预览版):允许在构造器主体中调用 super() 之前执行语句,从而提高了代码的可读性和灵活性。
- 454: 外来函数与内存 API:提供了一个纯 Java 应用程序接口,用于替代 JNI,以支持直接调用本地函数和操作内存,提高了性能和易用性。
- 456: 未命名变量与模式:允许使用下划线字符表示未命名变量和模式,增强了代码的可读性和可维护性。
- 457: 类文件 API(预览版):提供了一个用于操作类文件的 API,使开发人员能够更方便地处理 Java 字节码。
- 458: 启动多文件源代码程序:允许从命令行直接运行包含多个源文件的 Java 程序,简化了开发和测试过程。
- 459: 字符串模板(第二次预览版):进一步扩展了字符串模板的功能,提高了字符串操作的灵活性和可读性。
- 460: 向量 API(第七个孵化版):提供了用于高性能向量计算的 API,以支持现代硬件的 SIMD 指令集。
- 461: 流收集器(预览版):引入了一种新的流操作,用于将流元素收集到容器中,提高了流处理的灵活性。
- 462: 结构化并发(第二次预览版):提供了一种新的并发编程模型,简化了并发代码的编写和理解。
- 463: 隐式声明的类和实例主方法(第二次预览版):减少了初学者编写第一个 Java 程序时所需了解的复杂语言功能。
- 464: 作用域值(第二次预览版):提供了一种在代码块中定义作用域值的新机制,增强了代码的可读性和可维护性。
关于 JDK 22 的发布计划:
- 2023 年 12 月 7 日:进入第一阶段的减速期(从主线分叉)
- 2024 年 1 月 18 日:进入第二阶段的减速期
- 2024 年 2 月 8 日:发布初始候选版本
- 2024 年 2 月 22 日:发布最终候选版本
- 2024 年 3 月 19 日:达到通用可用性阶段
下面跟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: 未命名变量与模式
核心要点
:
- JEP 456 在 Java 语言中引入了未命名变量和未命名模式,使用下划线字符 _ 表示。
- 未命名变量用于在变量声明或嵌套模式需要但未使用时,明确开发者的意图,减少错误机会。
- 未命名模式允许在模式匹配中省略未使用的变量名,提高代码的可读性。
- 未命名变量和模式的使用不会影响变量的可见性,它们仅适用于局部变量、异常参数和 lambda 参数。
- 未命名模式可以嵌套在记录模式中,用于清晰的数据提取和模式匹配。
示例代码
:
// 使用未命名变量的增强 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(预览版)
核心要点
:
- 预览API: 提供了一个用于解析、生成和转换Java类文件的预览API。
- 类文件处理: 该API遵循Java虚拟机规范定义的类文件格式,用于处理类文件。
- JDK组件迁移: 旨在使JDK组件迁移到这个标准API,并最终移除JDK对第三方ASM库的依赖。
- 设计目标: 包括不可变对象表示、树状结构表示、用户驱动的导航、懒惰性、统一的流式和实体化视图、emergent transformation、细节隐藏以及利用Java语言的特性。
- 元素、构建器和转换: 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.java class Prog { public static void main(String[] args) { Helper.run(); } } // Helper.java class 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 来简化并发编程。以下是其核心要点和示例代码:
核心要点
:
- 结构化并发:将不同线程中运行的相关任务视为单个工作单元,以便更好地管理错误处理、取消操作、提高可靠性和增强可观测性。
- 子任务的生命周期管理:子任务的生命周期被限制在父任务的词法作用域内,确保子任务不会超出父任务的生命周期。
- 错误传播和取消:如果一个子任务失败,其他子任务可以自动被取消,并且父任务可以接收到错误,从而避免不必要的工作。
- 观察性工具:通过 JSON 线程转储格式扩展,可以更好地展示线程的层次结构,使得并发任务的调试和监控更加直观。
- 与虚拟线程的结合:结构化并发非常适合与虚拟线程一起使用,后者是 JDK 中实现的轻量级线程,可以高效地处理大量并发 I/O 操作。
示例代码
:
以下是一个使用 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 之后的第二轮预览功能,提供了一些重要的变化,并因此更新了标题。
核心要点
:
- 简化的程序入口点:允许使用非静态的 main 方法,不需要 public 修饰符,也不需要 String[] 参数。
- 隐式声明的类:源文件中没有包含类声明的方法和字段将被视为隐式声明的顶层类的一部分。这个类总是 final 的,并且只能继承 Object 类。
- 启动协议的灵活性:增强了 Java 虚拟机启动执行的协议,允许更灵活地声明程序的入口点,特别是允许实例 main 方法。
- 逐步过渡到复杂程序:允许初学者编写简单的程序,随着技能的提升,可以逐步引入更高级的语言特性。
- 无需特殊工具链:学生程序应使用与运行任何 Java 程序相同的工具进行编译和运行。
示例代码
:
以下是一个使用 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.java java --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.java java --enable-preview YourProgram
JEP 464 的作用域值提供了一种新的、改进的数据共享机制,特别适用于多线程和并发编程场景。
最后
以上是介绍 JDK22新特性的全部内容了,突然V哥想要感慨一下,技术之路,学无止境,选择 IT 技术,作个纯粹的人,享受研究技术的过程,这种带来的快感,也许只有真正热爱编程的人才能有体会。