1. 引言

在本教程中,我们将深入探讨最新的 Java 版本 Java 22,该版本现已正式发布。

让我们来谈谈作为此版本的一部分对 Java 语言的所有新更改。

2.1. 未命名的变量和模式 – JEP 456

我们经常定义代码中未使用的临时变量或模式变量。通常情况下,这是由于语言限制,禁止删除它们或引入副作用。异常、开关模式和 Lambda 表达式是我们在特定范围内定义变量或模式的示例,但我们从未使用过它们:

1
try { int number = someNumber / 0; } catch (ArithmeticException exception) { System.err.println("Division by zero"); } switch (obj) { case Integer i -> System.out.println("Is an integer"); case Float f -> System.out.println("Is a float"); case String s -> System.out.println("Is a String"); default -> System.out.println("Default"); } try (Connection connection = DriverManager.getConnection(url, user, pwd)) { LOGGER.info(STR.""" DB Connection successful URL = \{url} usr = \{user} pwd = \{pwd}"""); } catch (SQLException e) {}

未命名变量 (_) 在这种情况下是完美的,并且使变量的意图明确无误。它们不能在代码中传递,也不能使用或分配值。让我们重写前面的例子:

1
try { int number = someNumber / 0; } catch (ArithmeticException _) { System.err.println("Division by zero"); } switch (obj) { case Integer _ -> System.out.println("Is an integer"); case Float _ -> System.out.println("Is a float"); case String _ -> System.out.println("Is a String"); default -> System.out.println("Default"); } try (Connection _ = DriverManager.getConnection(url, user, pwd)) { LOGGER.info(STR.""" DB Connection successful URL = \{url} usr = \{user} pwd = \{pwd}"""); } catch (SQLException e) { LOGGER.warning("Exception"); }

2.2. super() 之前的语句 – JEP 447

很长一段时间以来,Java 不允许我们在子类的构造函数中调用 super() 之前放置任何语句。假设我们有一个 Shape 类系统和两个类,Square 和 Circle,从 Shape 类扩展而来。在子类构造函数中,第一个语句是对 super() 的调用:

1
public class Square extends Shape { int sides; int length; Square(int sides, int length) { super(sides, length); // some other code } }

当我们在调用 super() 之前执行某些验证时,这很不方便。在此版本中,解决了以下问题:

1
public class Square extends Shape { int sides; int length; Square(int sides, int length) { if (sides != 4 && length <= 0) { throw new IllegalArgumentException("Cannot form Square"); } super(sides, length); } }

我们应该注意,我们放在 super() 之前的语句不能访问实例变量或执行方法。我们可以用它来执行验证。此外,我们可以在调用基类构造函数之前使用它来转换派生类中接收的值。

这是预览 Java 功能。

3. 字符串模板 – JEP 459

Java 22 引入了 Java 流行的字符串模板功能的第二个预览版。字符串模板允许嵌入文本以及表达式和模板处理器,以生成专用结果。它们也是其他弦乐合成技术更安全、更有效的替代品。

在此版本中,字符串模板继续处于预览状态,自首次预览版以来对 API 进行了小幅更新。对模板表达式的键入进行了新的更改,以在模板处理器中使用相应 process() 方法的返回类型。

4. 隐式声明的类和实例主方法 – JEP 463

Java 最终支持编写程序,而无需使用其标准模板定义显式类或 main 方法。我们通常是这样定义一个类的:

1
class MyClass { public static void main(String[] args) { } }

开发人员现在可以简单地创建一个带有 main() 方法定义的新文件,如下所示并开始编码:

1
void main() { System.out.println("This is an implicitly declared class without any constructs"); int x = 165; int y = 100; System.out.println(y + x); }

我们可以使用其文件名对其进行编译。未命名的类驻留在未命名的包中,该包驻留在未命名的模块中。

 5. 图书馆

Java 22 还带来了新的库并更新了一些现有的库。

5.1. 外部函数和内存 API – JEP 454

Java 22 在经过几次孵化器迭代后最终确定了 Foreign Function and Memory API,作为 Project Loom 的一部分。此 API 允许开发人员调用外部函数,即 JVM 生态系统外部的函数,并访问 JVM 外部的内存。

它允许我们访问其他运行时和语言的库,这是 JNI(Java Native Interface)所做的,但效率更高、性能提升和安全性更高。此 JEP 为在运行 JVM 的所有平台上调用本机库提供了更广泛的支持。此外,该 API 内容广泛且更具可读性,并提供了跨多种内存类型(例如堆内存和瞬态内存)对无限大小的结构化和非结构化数据进行操作的方法。

我们将对 C 的 strlen() 函数进行本机调用,以使用新的 Foreign Function and Memory API 计算字符串的长度:

1
public long getLengthUsingNativeMethid(String string) throws Throwable { SymbolLookup stdlib = Linker.nativeLinker().defaultLookup(); MethodHandle strlen = Linker.nativeLinker() .downcallHandle( stdlib.find("strlen").orElseThrow(), of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)); try (Arena offHeap = Arena.ofConfined()) { MemorySegment str = offHeap.allocateFrom(string); long len = (long) strlen.invoke(str); System.out.println("Finding String length using strlen function: " + len); return len; } }

5.2. 类文件 API – JEP 457

类文件 API 标准化了读取、解析和转换 Java .class 文件的过程。此外,它的目标是最终弃用 JDK 的第三方 ASM 库的内部副本。

类文件 API 提供了几个强大的 API,用于有选择地转换和修改类中的元素和方法。举个例子,让我们看看如何利用 API 删除以 test_ 开头的类文件中的方法:

1
ClassFile cf = ClassFile.of(); ClassModel classModel = cf.parse(PATH); byte[] newBytes = cf.build(classModel.thisClass() .asSymbol(), classBuilder -> { for (ClassElement ce : classModel) { if (!(ce instanceof MethodModel mm && mm.methodName() .stringValue() .startsWith(PREFIX))) { classBuilder.with(ce); } } });

此代码分析源类文件的字节,并通过仅采用满足给定条件的方法(由 MethodModel 类型表示)来转换它们。可以验证生成的类文件,该文件省略了原始类的 test_something() 方法。

5.3. 流收集器 – JEP 461

JEP 461 通过 Stream::gather(Gatherer) 在 Streams API 中引入了对自定义中间操作的支持。开发人员长期以来一直希望支持其他操作,因为内置流中间操作有限。通过此增强功能,Java 允许我们创建自定义中间操作。

我们可以通过在流上链接 gather() 方法并为其提供 Gatherer 来实现这一点,Gatherer 是 java.util.stream.Gatherer 接口的一个实例。

让我们使用 Stream 收集器使用滑动窗口方法将元素列表分组为 3 个:

1
public List<List<String>> gatherIntoWindows(List<String> countries) { List<List<String>> windows = countries .stream() .gather(Gatherers.windowSliding(3)) .toList(); return windows; } // Input List: List.of("India", "Poland", "UK", "Australia", "USA", "Netherlands") // Output: [[India, Poland, UK], [Poland, UK, Australia], [UK, Australia, USA], [Australia, USA, Netherlands]]

作为此预览功能的一部分,有五个内置收集器:

  •  
  •  map并发
  •  扫描
  • windowFixed
  •  窗口滑动

此 API 还使开发人员能够定义自定义 Gatherer。

5.4. 结构化并发 – JEP 462

结构化并发 API 是 Java 19 中的孵化器特性,在 Java 21 中作为预览特性引入,并在 Java 22 中返回,没有任何新变化。

此 API 的目标是在 Java 并发任务中引入结构和协调。

结构化并发 API 旨在通过引入一种编码风格模式来改进并发程序的开发,该模式旨在减少并发编程的常见陷阱和缺点。

此 API 简化了错误传播,减少了取消延迟,并提高了可靠性和可观察性。

5.5. 作用域值 – JEP 464

Java 21 引入了作用域值 API 作为预览功能以及结构化并发功能。此 API 在 Java 22 中移动到第二个预览版中,没有任何更改。

作用域值允许在线程内和线程之间存储和共享不可变数据。作用域值引入了一种新类型 ScopedValue<>。我们写入一次值,它们在整个生命周期中保持不变。

Web 请求和服务器代码通常使用 ScopedValues。它们被定义为公共静态字段,并允许在方法之间传递数据对象,而无需定义为显式参数。

在以下示例中,让我们看看如何对用户进行身份验证,并将其上下文存储为跨多个实例的 ScopedValue:

1
private void serve(Request request) { User loggedInUser = authenticateUser(request); if (loggedInUser) { ScopedValue.where(LOGGED_IN_USER, loggedInUser) .run(() -> processRequest(request)); } } // In a separate class private void processRequest(Request request) { System.out.println("Processing request" + ScopedValueExample.LOGGED_IN_USER.get()); }

唯一用户的多次登录尝试将用户信息范围限定为其唯一线程:

1
Processing request :: User :: 46 Processing request :: User :: 23

5.6. Vector API(第七孵化器)– JEP 460

Java 16 引入了 Vector API,Java 22 带来了第七个孵化器。此更新提供了性能改进和次要更新。以前,矢量访问仅限于堆 MemorySegments,这些 MemorySegments 由字节数组提供支持。它们现在已更新为由一系列基元元素类型提供支持。

此更新是低级别的,不会以任何方式影响 API 的使用。

Java 22 更新了 Java 构建文件的工具。

6.1. 多文件源程序 – JEP 458

Java 11 引入了执行单个 Java 文件,而无需使用 javac 命令显式编译它。这非常有效和快速。缺点是,当存在依赖的 Java 源文件时,我们无法发挥其优势。

从 Java 22 开始,我们终于可以运行多文件 Java 程序了:

1
public class MainApp { public static void main(String[] args) { System.out.println("Hello"); MultiFileExample mm = new MultiFileExample(); mm.ping(args[0]); } } public class MultiFileExample { public void ping(String s) { System.out.println("Ping from Second File " + s); } }

我们可以直接运行 MainApp,而无需显式运行 javac:

1
$ java --source 22 --enable-preview MainApp.java "Test" Hello Ping from Second File Test

请记住以下几点:

  • 当类分散在多个源文件中时,无法保证编译顺序
  • 编译主程序引用其类的.java文件
  • 不允许在源文件中重复类,并且会出错
  • 我们可以传递 –class-path 选项来使用预编译的程序或库

 7. 性能

Java 的这次迭代带来的性能更新是对 G1 垃圾回收器机制的增强。

7.1. G1 垃圾回收器的区域固定 – JEP 423

固定是通知 JVM 的底层垃圾回收器不要从内存中删除特定对象的过程。不支持此功能的垃圾回收器通常会暂停垃圾回收,直到指示 JVM 释放关键对象。

这是一个主要见于JNI关键部分区域的问题。垃圾回收器中缺少固定功能会影响其在 JVM 中的延迟、性能和整体内存消耗。

在 Java 22 中,G1 垃圾回收器终于支持区域固定。这样,Java 线程就无需在使用 JNI 时暂停 G1 GC。

 8. 结论

Java 22 为 Java 带来了大量的更新、增强和新的预览功能。

像往常一样,所有代码示例都可以在 GitHub 上找到_._