Java 22 新功能特性及示例

Java 开发工具包 22 是 Java 标准版的下一个版本,现已于 2024 年 3 月 19 日正式发布。Java 22 具有 12 项重要功能,包括 7 项预览功能和 1 项孵化功能。它倾向于提高开发人员的工作效率和程序效率。在本文中,我们将通过示例探讨一些最基本的、对开发人员友好的 Java 22 新功能。

Java 22 新功能示例

让我们开始通过示例逐一了解 Java 22 的新功能。

无名变量和模式(JEP 456)

该功能在 Java 21 中以 “未命名模式和变量 “的名称作为预览功能推出,现在 Java 22 将其最终定名为 “未命名变量和模式”,未作任何更改。

很多时候,我们会定义一些实际上并不需要的变量或模式。在这种情况下,我们可以不给它们命名。相反,我们可以使用下划线 (_) 来表示这些未命名的元素。简单地说,如果我们声明的变量在代码中将不会被使用,我们就可以用下划线代替它们的名称。下划线表示该变量故意不使用。这一改进提高了代码的可读性。

例如,让我们来看看它是如何工作的:

示例#1:增强型 For 循环

public record Employee(String name) {}

for (Employee employee : employees) {
   total++;
   if (total > limit) {
     // logic
   }
}

如果我们使用未命名变量功能,就可以避免使用 employee 变量,而代之以下划线。

for (Employee _ : employees) {
   total++;
   if (total > limit) {
     //logic
   }
}

示例#2: Try-Catch 代码块

当我们要处理异常而不需要知道异常细节时,未命名的捕获块最有帮助

try {
   int number = Integer.parseInt(string);
} catch (NumberFormatException e) {
   System.err.println("Not a number");
}

我们可以将上述代码转换如下:

try {
   int number = Integer.parseInt(string);
} catch (NumberFormatException _) {
   System.err.println("Not a number");
}

它们也适用于同一捕获中的多种异常类型。例如

catch (IllegalStateException | NumberFormatException _) { }

示例#3: Lambda 参数

假设我们想创建一个简单的 lambda 表达式来打印一条消息。实际上我们并不需要参数值,因此可以使用一个未命名的变量:

 
   // Define a lambda expression with an unnamed parameter
   public static Map<String, List<Employee>> getEmployeesByFirstLetter(List<Employee> emps) {
      Map<String, List<Employee>> empMap = new HashMap<>();
      emps.forEach(emp ->
             empMap.computeIfAbsent(emp.name().substring(0, 1), _ -> new ArrayList<>()).add(emp)
      );
      return empMap;
   }

   // Use the lambda expression with an unnamed parameter
   map.forEach((_, _) -> System.out.println("Using unnamed Parameters"));

我们创建了一个接收参数(用 _ 表示)的 lambda 表达式。下划线 (_) 表示我们有意忽略参数值。
同样,我们也可以在其他情况下执行,如赋值语句、局部变量声明、方法引用、Try-With 资源等。

示例 #4:Switch 表达式中的未命名模式

未命名模式允许我们通过消除不必要的模式变量名来简化代码。

假设我们有一个带有三个record类的sealed接口 Employee:受薪雇员(Salaried)、自由职业者(Freelancer)和实习雇员(Intern)。我们希望以不同的方式处理每种类型的雇员。下面介绍如何在Switch表达式中使用未命名模式:

sealed interface Employee permits Salaried, Freelancer, Intern { }

record Salaried(String name, long salary) implements Employee { }
record Freelancer(String name) implements Employee { }
record Intern(String name) implements Employee { }

public class EmployeeProcessor {
    public static void main(String[] args) {
        Employee employee = new Freelancer("Alice");

        String employeeType = switch (employee) {
           case Salaried(_, _) -> "Salaried Employee";
           case Freelancer(_) -> "Freelancer";
           case Intern(_) -> "Intern";
           default -> "Unknown";
        };

        System.out.println("Employee type: " + employeeType);
    }
}

在本例中,我们定义了三个实现Employee接口的record类 Salaried、Freelancer 和 Intern。Switch表达式使用未命名模式,如下所示:

  • case Salaried(_, _) 会匹配任何 Salaried 雇员,但不绑定姓名或工资。
  • case Freelancer(_) 会匹配任何Freelancer雇员,但不绑定姓名。
  • case Intern(_) 会匹配任何Intern,但不绑定姓名。

默认情况处理其他情况。

启动多文件源代码程序(JEP 458)

Java 11 引入了直接使用 java launcher 运行单文件源代码程序而无需显式编译的功能。Java 22 的这一增强功能允许我们直接运行由多个 Java 源代码文件组成的 Java 程序,而无需事先显式编译。它简化了从小型程序到大型程序的过渡,允许开发人员选择何时必须配置构建工具。不过,这种方法也有局限性:所有代码都必须放在一个 .java 文件中。

让我们举例说明。假设我们有一个包含两个文件的目录:Employee.java 和 Helper.java,其中每个文件都声明了一个类:

// Employee.java
public class Employee{
   public static void main(String[] args) {
      Helper.run();
   }
}

// Helper.java
public class Helper {
    static void run() {
      System.out.println("Hello!");
    }
}

依据 JEP 458,我们现在就可以使用 java 启动器直接执行这个多文件程序了:

$ java Employee.java
Hello!

但是,从 Java 11 开始,只要我们进一步添加 Java 文件,所谓的 “启动单文件源代码 “机制就不再起作用。因此,Java 11 的 “启动单文件源代码 “功能在 Java 22 中变成了 “启动多文件源代码程序 “功能。

如果我们使用 Java 22 的这一功能,就可以在最初不配置构建工具的情况下进行实验。

JDK Enhancement Proposal 458 中定义了这一详细功能。它还解释了该功能在模块情况下的工作原理,以及如何处理理论上可能出现的一些特殊情况。不过,对于大多数情况来说,上述描述已经足够了。

super(…) 或 this(…) 之前的语句 [预览版,JEP 447]

在 Java 22 之前,子类的构造函数必须调用超类的构造函数作为初始化父类字段的第一条语句。Java 22 为第一条语句提供了灵活性,允许在调用超类构造函数之前调用其他语句。同样的规则也适用于构造函数链。

简单地说,我们可以在调用 super(…) 或 this(…) 之前在构造函数中执行代码。

这一修改的目的是为该规则提供灵活,允许在调用超类构造函数之前执行一些其他操作。

举例说明:Java 22 之前

public class Animal { 
   protected String name; 
   Animal(String name) { 
      this.name = name; 
   } 
} 

public class Dog { 
   Dog(String name, String animalName) { 
      System.out.println("call to super must be first statement in constructor"); // Error!!! 
      super(animalName); 
   } 
}

在上述示例中,如果我们试图在 super(…) 之前插入任何语句,编译器显然会出处。

举例说明:Java 22 之后

public class Animal { 
   protected String name; 
   Animal(String name) { 
      this.name = name; 
   } 
} 

public class Dog { 
   Dog(String name, String animalName) { 
      System.out.println("I am allowed before the call to super(...)"); // Valid since Java 22
      super(animalName); 
   } 
} 

如示例所示,从 Java 22 开始,允许在调用 super(…) 构造函数之前编写一条语句。

它为构造函数逻辑提供了更大的灵活性,尤其适用于为超类构造函数准备参数或验证构造函数参数。如果我们在 super(…) 之前使用语句,就能更自然地表达构造函数行为。需要包含在辅助静态方法、中间构造函数或构造函数参数中的逻辑现在可以更合理地放置。

流收集器 Stream Gatherers(预览版,JEP 461)

什么是 Stream Gatherer?

Gatherer 是一种将输入流转换为输出流的中间操作,输入流转换完成后,可选择执行最终操作。简单地说,Stream Gatherer 是扩展 Java 8 Stream API 功能的自定义中间操作。从技术上讲,Gatherer 是 Java 中的一个接口。为了创建Gatherer ,我们需要实现 Gatherer 接口。

为什么需要 Stream Gatherer?

现有的 Java Stream API 内置了一些固定的中间和终端操作集,用于映射、过滤、还原、排序等。使用这些固定的操作集无法轻松完成一些复杂的任务。Stream Gatherer将通过提供现有中间操作不足的操作集来帮助我们。

Stream Gatherer 有哪些优势?

Gatherer 为流管道提供了更多的灵活性和表现力。它们允许我们以以前复杂或现有内置中间操作不直接支持的方式操作数据流。它们提供了更多自定义流操作,提高了基于流任务的代码重用性。Gatherer 简化了对复杂流操作的理解和实现。

示例:不使用 Gatherer

让我们以一个复杂任务为例,该任务将元素分组为固定大小的三组,但只保留前两组。因此,流 stream  [0, 1, 2, 3, 4, 5, 6, …]应产生 [[0, 1, 2], [3, 4, 5]]。由于这种情况下没有中间操作,我们将通过编写下面的方法来实现:

public static ArrayList<ArrayList<Integer>> findGroupsOfThree(long fixed_size, int grouping) {

   return Stream.iterate(0, i -> i + 1)
                .limit(fixed_size * grouping)
                .collect(Collector.of(
          () -> new ArrayList<ArrayList<Integer>>(),
          (groups, element) -> {

          if(groups.isEmpty() || groups.getLast().size() == fixed_size) {
             var current = new ArrayList<Integer>();
             current.add(element);
             groups.addLast(current);
          }
          else {
             groups.getLast().add(element);
          }
   },

         (left, right) -> {
            throw new UnsupportedOperationException("Parallelization can't be done");
         }
   ));
}

输出:

[[0, 1, 2], [3, 4, 5]]

示例:使用 Gatherer

让我们考虑使用 Gatherer API 的 gather() 操作来简化 findGroupsOfThree() 方法:

public static List<List<Integer>> findGroupsOfThreeWithGatherer(long fixed_size, int grouping) {

    return Stream.iterate(0, i -> i + 1)

            .gather(Gatherers.windowFixed((int)fixed_size))

            .limit(grouping)

            .collect(Collectors.toList());
}

输出:

[[0, 1, 2], [3, 4, 5]]

如上例所示,我们使用了 JDK 22 中流 API 的一个新方法 gather()。Stream Gatherer API 定义了 Stream.gather(…) 方法和Gatherer 接口。

windowFixed() 方法返回一个将元素收集到窗口中的收集器,也就是说,我们可以将流元素按预定义的大小分组。

成功执行 findGroupsOfThreeWithGatherer(long, int) 方法后的输出结果是一样的:[[0, 1, 2], [3, 4, 5]]。毫无疑问,这样更易于阅读和维护。

要了解有关流收集器 Stream Gatherer 的更多详情,请访问 Gatherer 文档

外来函数与内存 API(JEP 454)

经过总共 8 个孵化器版本和 3 个预览版本之后,Java 22 中的外来函数与内存 API 终于由 JDK 增强提案 454 最终确定。该增强功能使 Java 程序能够与 Java 运行时之外的代码和数据互操作。

外来函数与内存 API(FFM API)使从 Java 访问外来函数(JVM 之外的代码)和外来内存(堆中不受 JVM 管理的内存)成为可能。它连接了 Java 与本地代码(如 C/C++ 库)之间的鸿沟。FFM API 的提出是为了取代高度复杂、容易出错且速度缓慢的 Java 本地接口 (JNI)。下面是一些例子:

示例 #1:从 C 库调用 strlen()

假设我们想从标准 C 库中调用 strlen() 函数。我们可以这样做

import jdk.incubator.foreign.CLinker;
import jdk.incubator.foreign.MemoryAddress;

public class StringLengthExample {
    public static void main(String[] args) {
       String text = "Hello, world!";
       MemoryAddress address = CLinker.toCString(text);
       long length = CLinker.systemDefault().lookup("strlen").invokeExact(address);
       System.out.println("Length of the string: " + length);
    }
}

示例 #2:使用 qsort() 对字符串排序

让我们使用 C 库中的 qsort() 函数对字符串数组进行排序。我们将提供一个 Java 回调函数来比较元素:

import jdk.incubator.foreign.CLinker;
import jdk.incubator.foreign.MemorySegment;
import java.util.Arrays;

public class StringSortExample {
    public static void main(String[] args) {
        String[] strings = {"Java", "Angular", "JavaScript", "Scala"};
        MemorySegment segment = CLinker.toCStringArray(strings);
        CLinker.systemDefault().lookup("qsort").invokeExact(segment, strings.length,
               CLinker.systemDefault().addressOf(String::compareTo));
        Arrays.stream(strings).forEach(System.out::println);
    }
}

Class-File API (预览版, JEP 457)

有时,有必要在不修改源代码的情况下检查和扩展程序。本增强功能为生成、解析和转换 Java 类文件提供了标准 API。它侧重于以更简便的方式处理类文件(字节码)。

Class-File API 允许开发人员根据 Java 虚拟机规范处理 class 类文件。它提供了处理 class 类文件的一致方法,从而弥合了 Java 与本地代码之间的差距。从长远来看,其目标是取代 JDK 内部对 ASM 等第三方库的复制。

类文件是 Java 生态系统的通用语言。现有的类文件库(如 ASM、BCEL、Javassist)具有不同的设计目标,进展速度也各不相同。JDK 的内部类文件库需要与不断变化的类文件格式保持同步。

举例说明:

让我们观察一下如何使用类文件 API 来读取和分析类文件:

import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;
import jdk.incubator.foreign.ClassFile;
import jdk.incubator.foreign.ClassFileParser;

public class ClassFileExample {
    public static void main(String[] args) throws IOException {
       Path classFilePath = Paths.get("MyClass.class");
       ClassFile classFile = ClassFileParser.parse(classFilePath);

       // Extract class name and interfaces
       String className = classFile.thisClass().name();
       List<String> interfaces = classFile.interfaces().stream()
                  .map(ClassFile.ConstPoolEntry::name)
                  .collect(Collectors.toList());

       System.out.println("Class name: " + className);
       System.out.println("Interfaces: " + interfaces);
    }
} 

在本例中,我们使用类文件 API 解析了一个类文件 (MyClass.class)。我们提取了类文件中定义的类名和接口。

为了尝试 JDK 22 中的示例,我们必须启用预览功能,具体如下:

Compile the program with javac --release 22 --enable-preview Main.java.
Run it with java --enable-preview Main.

总之,类文件应用程序接口(Class-File API)简化了类文件处理过程,确保了与不断发展的类文件格式的兼容性,并为字节码的处理提供了一种一致的方法。

Scoped 值(第二次预览版,JEP 464)

Java 21 中引入了作用域(Scoped )值作为预览功能。Java 22 中再次将作用域值作为第二次预览,未作任何更改。

作用域值允许将一个或多个值传递给一个或多个方法,而无需将其明确定义为参数。它们是线程本地变量的替代品,尤其是在处理大量虚拟线程时。作用域值因其安全性和高效性而备受青睐。

Vector API(第七次孵化,JEP 460)

该增强功能旨在提供一个 API,用于表达矢量计算,并在运行时编译为所支持 CPU 架构上的最佳矢量指令。从本质上讲,它允许开发人员高效地处理矢量化操作。Vector API 可以使用简洁且与平台无关的 API 来表达向量计算。该 API 专为提高性能和可移植性而设计。

由于该功能三年多来仍处于孵化阶段,我们应该等待它进入预览阶段后的进一步细节。

本文文字及图片出自 Java 22 New Features With Examples

余下全文(1/3)
分享这篇文章:

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注