编程

Java 21 的新特性

253 2024-08-16 05:00:00

1. 概述

本文将探讨 Java 21 的新特性和增强。

Java 21 于 2023 年 9 月发布,是 Java 17 之后的 最新 LTS 版本。

2. JEP 列表

接下来,我们谈谈新版本中引入的最显著的增强,也叫 Java Enhancement Proposals (JEP)。

2.1. Record Patterns (JEP 440)

记录模式(Record Pattern)作为预览功能在 Java 19 和 Java 20 中引入。现在,在 Java 21 中,它们已经脱离了预览版,并引入了一些改进。

此 JEP 扩展了现有的模式匹配特性,以析构 Record 类实例,从而能够编写复杂的数据查询。此外,该 JEP 还添加了对嵌套模式的支持,以实现更可组合的数据查询。

例如,我们可以更简洁地编写代码:

record Point(int x, int y) {}
    
public static int beforeRecordPattern(Object obj) {
    int sum = 0;
    if(obj instanceof Point p) {
        int x = p.x();
        int y = p.y();
        sum = x+y;
    }
    return sum;
}
    
public static int afterRecordPattern(Object obj) {
    if(obj instanceof Point(int x, int y)) {
        return x+y;
    }
    return 0;
}

因此,我们还可以将记录嵌套在记录中并对其进行析构:

enum Color {RED, GREEN, BLUE}
record ColoredPoint(Point point, Color color) {}
record RandomPoint(ColoredPoint cp) {}
public static Color getRamdomPointColor(RandomPoint r) {
    if(r instanceof RandomPoint(ColoredPoint cp)) {
        return cp.color();
    }
    return null;
}

以上我们直接从 ColoredPoint 中访问了 .color() 方法。 

2.2. switch 的模式匹配(JEP 441)

最初在 JDK 17 中引入,switch 的模式匹配在 JDK 18、19 和 20 中进行了改进,并在 JDK 21 中进一步改进。

该特性的主要目标是在 switch case 标签中允许模式并改进 switch 语句和表达式的表现力。此外,还有一个增强,用以通过允许 null 的 case 标签来处理 NullPointerException

让我们通过示例来探讨这一点。假设我们有一个 Account 类:

static class Account{
    double getBalance(){
       return 0;
    }
}

我们将其扩展为多个不同类型的 account,每个都有用于计算余额的方法:

static class SavingsAccount extends Account {
    double getSavings() {
        return 100;
    }
}
static class TermAccount extends Account {
    double getTermAccount() {
        return 1000;
    } 
}
static class CurrentAccount extends Account {
    double getCurrentAccount() {
        return 10000;
    } 
}

在 Java 21 之前,我们可以使用以下代码来获取余额:

static double getBalanceWithOutSwitchPattern(Account account) {
    double balance = 0;
    if(account instanceof SavingsAccount sa) {
        balance = sa.getSavings();
    }
    else if(account instanceof TermAccount ta) {
        balance = ta.getTermAccount();
    }
    else if(account instanceof CurrentAccount ca) {
        balance = ca.getCurrentAccount();
    }
    return balance;
}

由于我们使用了许多烦人的 if-else,上述代码不太有表现力。而在 Java 21 中,我们可以利用  case 标签中的模式来更简洁地编写相同的逻辑:

static double getBalanceWithSwitchPattern(Account account) {
    double result = 0;
    switch (account) {
        case null -> throw new RuntimeException("Oops, account is null");
        case SavingsAccount sa -> result = sa.getSavings();
        case TermAccount ta -> result = ta.getTermAccount();
        case CurrentAccount ca -> result = ca.getCurrentAccount();
        default -> result = account.getBalance();
    };
    return result;
}

为了验证我们的代码是正确的,让我们使用测试来证明:

SwitchPattern.SavingsAccount sa = new SwitchPattern.SavingsAccount();
SwitchPattern.TermAccount ta = new SwitchPattern.TermAccount();
SwitchPattern.CurrentAccount ca = new SwitchPattern.CurrentAccount();

assertEquals(SwitchPattern.getBalanceWithOutSwitchPattern(sa), SwitchPattern.getBalanceWithSwitchPattern(sa));
assertEquals(SwitchPattern.getBalanceWithOutSwitchPattern(ta), SwitchPattern.getBalanceWithSwitchPattern(ta));
assertEquals(SwitchPattern.getBalanceWithOutSwitchPattern(ca), SwitchPattern.getBalanceWithSwitchPattern(ca));

所以,我们只是更简洁地重写了一个 swicth。

模式 case 标签还支持与同一标签的表达式进行匹配。例如,假设我们需要处理一个包含简单 “Yes” 或 “No“ 的输入字符串:

static String processInputOld(String input) {
    String output = null;
    switch(input) {
        case null -> output = "Oops, null";
        case String s -> {
            if("Yes".equalsIgnoreCase(s)) {
                output = "It's Yes";
            }
            else if("No".equalsIgnoreCase(s)) {
                output = "It's No";
            }
            else {
                output = "Try Again";
            }
        }
    }
    return output;
}

同样,我们可以看到编写 if-else 逻辑会变得丑陋。而在 Java 21 中,我们可以在 case 标签中使用 when 语句来匹配标签的值: 

static String processInputNew(String input) {
    String output = null;
    switch(input) {
        case null -> output = "Oops, null";
        case String s when "Yes".equalsIgnoreCase(s) -> output = "It's Yes";
        case String s when "No".equalsIgnoreCase(s) -> output = "It's No";
        case String s -> output = "Try Again";
    }
    return output;
}

2.3. 字符串字面量(JEP 430)

Java 提供了多种将字符串与字符串字面量和表达式组合的机制。比如 String 连接、StringBuilder 类、Stringformat() 方法和 MessageFormat 类。

Java 21 引入了字符串模板。这通过将字面量文本与模板表达式和模板处理器耦合来产生所需的结果,从而补充了 Java 现有的字符串字面量和文本块。

让我们来看一个例子:

String name = "World";
String welcomeText = STR."Welcome to \{name}";
System.out.println(welcomeText);

上述代码打印了文本 “Welcome to World“.

在上面的文本中,我们有一个模板处理器(STR)、一个点字符和一个包含嵌入式表达式(\{name})的模板。在运行时,当模板处理器计算模板表达式时,它将模板中的字面量文本与嵌入表达式的值组合在一起以产生结果。

STR 是 Java 的模板处理器之一,它会自动导入到所有 Java 源文件中。Java 还提供 FMT 和 RAW 模板处理器。

2.4. 虚拟线程 (JEP 444)

虚拟线程最初是作为 Java 19 中的预览特性引入 Java 语言的,并在 Java 20 中进一步改进。Java 21 引入了一些新的变化。

虚拟线程是轻量级线程,目的是减少开发高并发应用的工作量。传统线程,也称为平台线程,是围绕操作系统线程的薄包装器。平台线程的一个主要问题是,它们在操作系统线程上运行代码,并在整个生命周期内捕获操作系统线程。操作系统线程的数量是有限的,这造成了可扩展性瓶颈。

与平台线程一样,虚拟线程也是 java.lang.thread 类的实例,但它不与特定的操作系统线程绑定。它在特定的操作系统线程上运行代码,但不会在整个生命周期内捕获线程。因此,许多虚拟线程可以共享操作系统线程来运行它们的代码。

让我们用一个示例来看看虚拟线程的使用:

try(var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.rangeClosed(1, 10_000).forEach(i -> {
        executor.submit(() -> {
            System.out.println(i);
            try {
                Thread.sleep(Duration.ofSeconds(1));
            } 
            catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    });
}

上述代码中,我们使用静态的 newVirtualThreadPerTaskExecutor() 方法。该执行器为每个任务创建了虚拟线程,因此上例中,我们创建了 10,000 个虚拟线程。

Java 21 引入了两个显著更改到虚拟线程中:

  • 现在虚拟线程总是支持线程本地变量。
  • 通过 Thread.Builder API 创建的虚拟线程在其生命周期内也会受到监控,并在新线程 dump 中可观察到

2.5. 有序集合 (JEP 431)

在 Java 集合框架中,没有集合类型表示具有定义的顺序的元素序列。例如,ListDeque 接口定义了顺序,但它们通用的超类型 Collection 没有。同样,Set 不定义顺序,但 LinkedHashSetSortedSet 等子类型定义了顺序。

Java 21 引入了三个新接口来表示有序集合、有序 set 和有序映射。

有序集合是指其元素具有定义的顺序的集合。它有首个元素和最后一个元素,它们之间的元素有前后顺序。有序 Set 是一个没有重复元素的有序集合。有序映射是指其条目具有定义的顺序的映射。

下图显示了集合框架层次结构中新引入的接口的改装:

2.6. 密钥封装机制(Key Encapsulation Mechanism) API (JEP 452)

密钥封装是一种使用非对称密钥或公钥密码学来保护对称密钥的技术。

传统方法使用公钥来保护随机生成的对称密钥。然而,这种方法需要填充,这很难证明是安全的。

密钥封装机制(KEM)使用公钥来导出不需要任何填充的对称密钥。

Java 21 引入了新的 KEM API,使应用能够使用 KEM 算法。

3. 结论

本文中,我们讨论了 Java 21 中交付的一些值得注意的更改。

我们讨论了记录模式、switch 的模式匹配、字符串模板、有序集合、虚拟线程和新的 KEM API。

其他增强和改进分布在 JDK 21 包和类中。