Blog chevron_right 未分类

JDK 22,一个(在某种意义上)很“平均”的版本

今天,Java 的最新版本 JDK 22 如同原子钟般规律地如约而至。尽管这不是一个长期支持 (LTS) 版本,但也没有什么理由阻止您在生产环境中使用它,而且,它还包含一些值得关注的新功能。

让我们来深入了解一下这个版本有什么内容。

Java 平台的新功能是通过 JDK 增强提案 (JEP) 定义的,JDK 22 实现了其中的 12 项提案。巧合的是,自从将发布周期改为 6 个月以来,在之前的 13 个 Java 版本中,每个版本包含的 JEP 的平均数量(取整数)也是 12。所以可以说,JDK 22 是个很“平均”的版本!

进一步了解 JDK 22

Event

JDK 22: Twice as Good as JDK 11

March 27th at 10am PT | Virtual
Research & White Papers

Azul State of Java Survey and Report 2023

进一步拆分后,我们可以看到四个最终功能,以及八个预览功能或孵化模块。

Java 语言的变化

JEP 447super() 之前的语句(预览)。作为一种面向对象的语言,Java 支持扩展另一个(非 final)类,并继承其状态和行为(在允许的情况下)。为了可靠地实现这一点,必须按自上而下的顺序调用类的构造函数,以避免子类构造函数干扰超类构造函数。如果开发人员决定显式调用超类构造函数,则这条调用语句必须是第一条语句。

这会带来一些限制,导致代码比预期更复杂。其中最显著的是,在调用超类构造函数之前,无法对构造函数的参数进行测试。如果测试导致抛出异常,则对超类构造函数的调用就成了技术上不必要的开销。

该 JEP 引入了一种受控的方法,允许在调用 super() 之前执行语句。对 thissuper 对象引用的任何引用都会产生错误,因为被引用的对象尚未完全初始化。JEP 中还详细描述了其他几项限制。

JEP 456:不具名模式和变量。该功能在 JDK 21 中作为预览功能推出,目前已成为最终功能,无任何改动。

该功能允许开发人员使用单个下划线“_”来表示不会被使用的变量。我们可以通过这种方式,确保以正确的语法构造代码,并提高其可读性,避免具名变量导致代码杂乱无章、难以理解的情况。

foreach 风格的循环就是一个简单的例子,它会遍历集合,但不会使用检索到的值:

 for (String name : userList)
                userCount++;

可以简化为:

            for (String _ : userList)                 userCount++;

这是一个不起眼的例子,但如果应用到模式中,它的价值就会变得更显著。使用组合的记录模式(一种解构模式)时,我们可能会编写这样的代码:

            public void printTopLeftX(Rectangle r) {
                 if (r instanceof ColourRectangle(ColourPoint(
                       Point(double x, double y), Colour c),
                       ColourPoint bottomRight))
                 System.out.println(x);
                 }

由于我们只使用 x 的值,其他所有变量(包括它们的类型定义)都是多余的,会使代码变得复杂难懂。  使用不具名模式,我们可以这样重写:

            public void printTopLeftX(Rectangle r) {
                 if (r instanceof ColourRectangle(ColourPoint(Point(var x, _), _), _) )
            System.out.println(x);
            }

它们也可以在 switch 和 lambda 表达式的模式匹配中使用。

JEP 459:字符串模板(预览)。该功能在 JDK 21 中作为预览功能推出,目前仍处于预览阶段,无开发人员可见的改动。

在 Java 中,构建字符串的方式有多种,包括:通过字符串拼接,以及使用 StringBuilder 等库类和 String.format()。我们可以使用字符串模板,以一种简单的方式表达包含了将在运行时计算的值的字符串。

现在,我们有了一个字符串模板处理器 STR,它可以对字符串执行插值操作。

举个简单的例子,我们可以这样插入值:

            String name = "Simon";
                 String info = STR." My name is \{name}";

字符串模板的真正强大之处在于,它支持更复杂的插值,例如使用方法调用:

            String s = STR." We are here \{
                      // Method call
                 DateTimeFormatter
                           .ofPattern("B")
                           .format(LocalTime.now())
                 } learning Java";

请注意,我们可以随意缩进代码,这并不会影响生成的字符串,甚至还可以加入注释。

JEP 463:隐式声明的类和实例主方法(预览)。此功能在 JDK 21 中作为预览功能推出,在引入一些重大更改后,目前仍处于预览状态。

即使是编写最简单的 Java 应用程序,也需要遵循大量繁琐的规范(编写样板代码)。对于传统的“hello world”程序,我们需要编写所有这些代码:

             public class Hello {

                  public static void main(String[] args) {

                       System.out.println(“Hello, World!”);

                  }

             }

此 JEP 允许程序使用一个不需要标记为 static 或 public 的实例 main 方法。它还提供了一个编译单元,让我们不再需要显式地定义类。  这些更改的整体效果是将我们的应用程序简化成了:

             void main(){

                  System.out.printIn("Hello, World!");

             }

这对于经验丰富的开发人员用处有限,但对 Java 新手有帮助。

库的变化

JEP 454:外部函数和内存 API该功能最初在 JDK 19 中作为预览功能推出,目前已成为最终功能。

该 API 是巴拿马项目 (Project Panama) 的一部分,它为 Java 本地接口 (JNI) 提供了更简单的替代方案。这些 API 提供了与本地内存,以及直接编译成本地代码(而非字节码)的外部库进行交互的标准方式。通过下行调用(从 Java 到本地函数)和上行调用(从本地代码到 Java 代码的回调),可以实现双向交互。

此外,还可以使用附加的 jextract 工具生成访问外部库的代码。JDK 默认不包含该工具,但 OpenJDK 以源代码的形式提供它。

JEP 457:类文件 API(预览)。大多数开发者可能不会关注这个 API。JVM 为了加载和运行应用程序,显然必须对类文件进行处理,此外,许多框架和库也需要处理类文件。

此 API 旨在为用户与类文件的交互提供一种标准化和简化的方式。

JEP 460Vector API(预览)。该 API 保持着预览时间最长的记录,在 JDK 22 中,其已进入了第七个迭代版本。

在这里,“Vector”是指所有现代 CPU 中都提供的大位宽寄存器。使用一种称为“单指令、多数据”(SIMD) 的技术,可以将数组中的多个元素加载到矢量中,并在一个时钟周期内执行相同的操作(例如增加或减去一个值)。这种并行处理可以显著提高数值密集型运算的性能。

通过这个 API,开发人员可以指定如何将值存储到矢量中,以及需要对其执行的操作。

对于简单的情况,JVM 内部的即时 (JIT) 编译器将识别可以使用矢量运算的情境,并自动应用它们。而对于较复杂的情况(例如,循环中包含对计算的条件测试),这个 API 将会有所帮助。

另外,Azul Platform Prime Zing JVM 使用了一个替代版本的 JIT 编译器,名为 Falcon。Falcon 基于开源的 LLVM 项目,能够识别更多可以使用矢量运算的情况,并在无需修改代码的前提下提供了更好的性能。

JEP 461:流聚合器(预览)。JDK 8 中引入了流 API,它与 lambda 表达式相结合,提供了一种偏函数式的编程风格,这在之前的 Java 版本中是无法实现的。

流包含一个源,在使用终端操作生成结果或副作用之前,可以通过零个或多个中间操作传递源提供的元素。

对于终端操作,可以通过 Stream::collector 定义自己的功能。  尽管有一套丰富的中间操作,但它们是固定的,开发人员无法对其进行扩展。

现在,流聚合器 API 提供了定义新中间操作的方式。

聚合器由四个函数定义:可选的初始化器、集成器、可选的组合器和可选的完成器。使用它们,开发人员可以完全灵活地处理聚合器的输入元素,并将生成的输出传递给下一个中间操作或终端操作。

JEP 462:结构化并发(预览)。该功能在 JDK 19 中作为孵化模块首次引入,自 JDK 21 起成为预览功能。此版本未对其做出任何更改。

归根结底,编写可靠的多线程代码极具挑战性。纵观 Java 的历史,许多新功能的诞生目标是为了至少在某种程度上简化这项任务。并发工具、fork-join 框架,和并行流为不同类型的多线程情境提供了解决方案。

结构化并发是 Loom 项目的一部分,它将运行在不同线程中的多项任务视作一个工作单元。这简化了错误处理和取消。

JEP 464:作用域值(预览)。此功能最初在 JDK 21 中作为预览功能推出。在 JDK 22 中无更改,目前仍处于预览状态。

作用域值也与 Loom 项目相关,它提供了一种替代线程局部变量的选择。作用域值与线程局部变量的关键区别在于,作为值(而非变量),它们是不可变的。

线程局部变量的复杂性通常超出了实际需要的程度,并且与之相关的资源成本相当高。由于虚拟线程支持更高的可扩展性,比平台线程高出几个数量级,因而这种资源成本可能会成为一种限制性因素。作用域值解决了这一问题,使开发人员能够灵活处理多线程应用程序逻辑。

JVM 和其他更改

JEP 423G1 区域固定。这是对 G1 垃圾收集器工作方式的改进,可以在使用 JNI 时降低延迟。

与 Java 代码不同,JNI 交互可能会使用显式指针指向堆中的对象。当 Java 线程执行 JNI 代码的关键区域时,无法将这些对象重新移动到堆内的其他位置。为了避免这个问题,G1 在关键区域禁用了垃圾收集器,这可能会导致延迟增加,因为如果其他非 JNI 线程触发了垃圾收集器,将导致线程阻塞。

这项更改固定了 JNI 代码使用的内存区域,但允许在其他内存区域移动对象和收集垃圾,从而使 G1 垃圾收集器即使在线程处于关键区域时仍可运行。

JEP 458:启动多文件源代码程序。在 JDK 11 中,JEP 330 引入了无需编译即可直接运行单个源文件的功能。这与上文 JEP 463 中讨论的简化 Java 入门有关。

该 JEP 增强了 Java 应用程序启动器,允许其运行由多个(而不仅仅是一个)Java 源代码文件组成的程序。大多数开发人员会使用 IDE 开发项目,使用 jshell 实验代码片段。 

这项功能对于刚开始学习 Java 编程的人最为有用。

结论

如您所见,JDK 22 所含新功能的数量只是达到了平均水平,但它们带来的价值却高于一般水准。Azul 提供了 Zulu OpenJDK 社区版的免费下载,您可以安装和试验。不妨来尝试一下吧!