Java 21 于 2023 年 9 月 19 日发布,是 Oracle 标准 Java 实现的下一个长期支持(LTS)版本。Java 21 具有以下 15 项功能。
字符串模板(预览版) [JEP-430]
序列集合 [JEP-431]
代 ZGC [JEP-439]
记录模式 [JEP-440]
开关的模式匹配 [JEP-441]
外来函数和内存 API(第三次预览) [JEP-442]
未命名模式和变量(预览) [JEP-443]
虚拟线程 [JEP-444]
未命名类和实例主方法(预览) [JEP-445]
作用域值(预览) [JEP-446]
矢量 API(第六期孵化器) [JEP-448]
停用 Windows 32 位 x86 端口,以便删除 [JEP-449]
准备禁止动态加载代理 [JEP-451]
密钥封装机制 API [JEP-452]
结构化并发(预览版) [JEP-453]
1.虚拟线程(Project Loom)
虚拟线程是由 JVM 管理的轻量级线程,有助于编写高吞吐量并发应用程序(吞吐量指系统在给定时间内能处理多少个信息单位)。[JEP-425、JEP-436 和 JEP-444]在 Java 21 中,虚拟线程已可投入生产使用。
引入虚拟线程后,只需使用几个操作系统线程就能执行数百万个虚拟线程。最有利的一点是,无需修改现有的 Java 代码。只需指示我们的应用程序框架使用虚拟线程来代替平台线程即可。
要使用 Java API 创建虚拟线程,我们可以使用线程或执行器。
Runnable runnable = () -> System.out.println("Inside Runnable");
//1
Thread.startVirtualThread(runnable);
//2
Thread virtualThread = Thread.ofVirtual().start(runnable);
//3
var executor = Executors.newVirtualThreadPerTaskExecutor();
executor.submit(runnable);
请注意,虚拟线程并不比平台线程更快。虚拟线程应该用于扩展大部分时间都在等待的并发任务的数量。例如,处理大量客户端请求和执行阻塞 I/O 操作的服务器应用程序。对于资源/处理密集型任务,应继续使用传统的平台线程,因为虚拟线程不会带来任何优势。
此外,要注意的是,更多的线程意味着依赖更多的系统资源,而这些资源可能无法按比例扩展。为了防止资源耗尽并确保最佳的系统利用率,我们必须使用 Semaphore 等机制限制并发线程的数量来进行测试。
2.Sequenced Collections
在有序集合计划下创建的新界面表示具有定义的相遇顺序的集合。该顺序将有明确定义的第一个元素、第二个元素,依此类推,直到最后一个元素。新添加的接口提供了统一的应用程序接口,可按顺序或相反顺序访问这些元素。
现在,所有流行和常用的集合类都根据相应的集合类型实现了 java.util.SequencedCollection、java.util.SequencedSet 或 java.util.SequencedMap 接口。
新的接口有额外的方法来支持对元素的顺序访问。例如,SequencedCollection 有以下方法:
interface SequencedCollection<E> extends Collection<E> {
// new method
SequencedCollection<E> reversed();
// methods promoted from Deque
void addFirst(E);
void addLast(E);
E getFirst();
E getLast();
E removeFirst();
E removeLast();
}
请注意,对原始集合的任何修改在反转集合视图中都是可见的。让我们通过一个 Java 程序来了解如何使用新的有序 ArrayList:
ArrayList<Integer> arrayList = new ArrayList<>();
addFirst.add(1); // [1]
arrayList.addFirst(0); // [0, 1]
arrayList.addLast(2); // [0, 1, 2]
arrayList.getFirst(); // 0
arrayList.getLast(); // 2
要了解其优势,请参阅 Java 17 中这些简单操作如何过于繁琐。
arrayList.get( arrayList.iterator().next() ); // first element
arrayList.get( arrayList.size() - 1 ); // last element
3.Record Patterns
Java 中的record是透明且不可更改的数据载体(类似于 POJO)。我们创建记录的过程如下:
record Point(int x, int y) {}
以前,如果我们需要访问记录的组件,应按如下方式对其进行重组:
if (obj instanceof Point p) {
int x = p.x();
int y = p.y();
System.out.println(x+y);
}
在 Java 21 中,我们可以使用 Point(int x, int y)语法(称为记录模式)以更简洁的方式重写它。
记录模式省去了为提取的组件声明局部变量的过程,当一个值与模式匹配时,通过调用访问器方法来初始化组件。
if (obj instanceof Point(int x, int y)) {
System.out.println(x+y);
}
4.Pattern Matching for switch
从 Java 21 开始,我们可以在 switch 语句中使用记录模式。请注意,switch 块必须包含处理选择表达式所有可能值的子句。
例如,在 Java 16 中,我们可以这样做
record Point(int x, int y) {}
public void print(Object o) {
switch (o) {
case Point p -> System.out.printf("o is a position: %d/%d%n", p.x(), p.y());
case String s -> System.out.printf("o is a string: %s%n", s);
default -> System.out.printf("o is something else: %s%n", o);
}
}
在 Java 21 中,我们可以用记录模式写出类似的表达式,如下所示:
public void print(Object o) {
switch (o) {
case Point(int x, int y) -> System.out.printf("o is a position: %d/%d%n", x, y);
case String s -> System.out.printf("o is a string: %s%n", s);
default -> System.out.printf("o is something else: %s%n", o);
}
}
5.String Templates (Preview)
使用字符串模板,我们可以创建包含嵌入式表达式(运行时求值)的字符串模板。模板字符串可以包含变量、方法或字段,在运行时进行计算,生成格式化的字符串作为输出。
从语法上讲,模板表达式类似于带前缀的字符串字面量。
let message = "Greetings {{ name }}!"; //TypeScript
String message = STR."Greetings \{ name }!"; //Java
在上述模板表达式中
STR 是模板处理器。
在处理器和表达式之间有一个点运算符(.)。
内嵌表达式的模板字符串。表达式的形式是 (\{name})。
请注意,模板处理器的结果以及模板表达式的求值结果通常是字符串,但不一定总是字符串。
6.未命名模式和变量(预览)
在其他一些编程语言(如 Scala 和 Python)中,我们可以跳过对将来不会使用的变量的命名。现在,从 Java 21 开始,我们也可以在 Java 中使用未命名/未使用的变量了。
让我们通过一个例子来理解。我们通常会如下处理异常:
String s = ...;
try {
int i = Integer.parseInt(s);
//use i
} catch (NumberFormatException ex) {
System.out.println("Invalid number: " + s);
}
请注意,我们创建了变量 ex,但没有在任何地方使用它。在上例中,变量未被使用,其名称也无关紧要。未命名变量的特性允许我们跳过变量的名称,而直接使用下划线 (_) 来代替它。下划线表示没有变量名。
String s = ...;
try {
int i = Integer.parseInt(s);
//use i
} catch (NumberFormatException _) {
System.out.println("Invalid number: " + s);
}
同样,我们也可以在开关表达式中使用未命名变量:
switch (obj) {
case Byte, Short, Integer, Long _ -> System.out.println("Input is a number");
case Float, Double _ -> System.out.println("Input is a floating-point number");
case String _ -> System.out.println("Input is a string");
default -> System.out.println("Object type not expected");
}
有了未命名模式,我们还可以在记录模式中使用未命名变量。让我们将前面的例子中用于开关表达式的记录模式与未命名变量混合使用,从而产生未命名模式。
在下面的示例中,我们不使用 y 的值,因此可以简单地将其声明为未命名变量。
public void print(Object o) {
switch (o) {
case Point(int x, int _) -> System.out.printf("The x position is : %d%n", x); // Prints only x
//...
}
}
7.未命名类和实例主方法(预览)
这是预览语言功能,默认情况下是禁用的。要使用它,我们必须使用 --enable-preview 标志启用预览功能。
在 Java 中,未命名模块和包是一个熟悉的概念。如果我们不创建 module-info.java 类,编译器就会自动假定该模块。同样,如果我们不在根目录下的类中添加包语句,类就会被编译并正常运行。
同样,我们现在可以创建未命名的类。很明显,未命名类就是没有名称的类。
请看下面的类声明,我们通常创建它来测试代码片段或一个简单的概念。
public class TestAConcept {
public static void main(String[] args) {
System.out.println(method());
}
static String method() {
//...
}
}
在 Java 21 中,我们可以不使用类声明来编写上述类,如下所示。它删除了类声明、public 和 static 访问修饰符等,使类更加简洁。
void main(String[] args) {
System.out.println(method());
}
String method() {
//...
}
在 Java 21 中,我们可以使用命令创建上述类。请注意,我们已将该类保存在 TestAConcept.java 文件中。
$ java --enable-preview --source 21 TestAConcept.java
8.Scoped Values(预览)
如果你熟悉 ThreadLocal 变量,那么作用域值就是一种在线程内和线程间共享数据的现代方式。作用域值允许在有限的时间内存储一个值(对象),只有写入该值的线程才能读取该值。
作用域值通常创建为公共静态字段,因此我们可以直接访问它们,而无需将其作为参数传递给任何方法。但是,我们必须明白,如果该值在多个方法中被检查,那么当前值将取决于线程的执行时间和状态。在不同方法中访问时,该值可能会随时间而改变。
要创建作用域值,请使用 ScopedValue.newInstance() 工厂方法。
public final static ScopedValue<USER> LOGGED_IN_USER = ScopedValue.newInstance();
通过 ScopedValue.where(),我们将作用域值与对象实例绑定,然后运行一个方法,在该方法的调用持续时间内,作用域值应该有效。请注意,作用域值只写入一次,然后不可更改,因此任何人都无法在调用的方法中更改登录用户。
class LoginUtil {
public final static ScopedValue<USER> LOGGED_IN_USER = ScopedValue.newInstance();
//Inside some method
User loggedInUser = authenticateUser(request);
ScopedValue.where(LOGGED_IN_USER, loggedInUser).run(() -> service.getData());
}
在被调用的线程中,我们可以直接访问作用域值:
public void getData() {
User loggedInUser = LoginUtil.LOGGED_IN_USER.get();
//use loggedInUser
}
9.结构化并发(预览)
结构化并发功能旨在将运行在不同线程(分叉自同一父线程)中的多个任务视为一个工作单元,从而简化 Java 并发程序。将所有此类子线程视为单一工作单元有助于将所有线程作为一个单元进行管理,从而更可靠地进行取消和错误处理。
在结构化多线程代码中,如果一个任务分成多个并发子任务,它们都会返回到同一个地方,即任务的代码块。这样,并发子任务的生命周期就仅限于该语法块。
在这种方法中,子任务代表任务工作,而任务则等待子任务的结果并监控子任务是否出现故障。在运行时,结构化并发会建立一个树形的任务层次结构,同级子任务归属于同一个父任务。这棵树可以看作是单线程调用堆栈的并发对应物,其中有多个方法调用。
try (var scope = new StructuredTaskScope.ShutdownOnFailure()()) {
Future<AccountDetails> accountDetailsFuture = scope.fork(() -> getAccountDetails(id));
Future<LinkedAccounts> linkedAccountsFuture = scope.fork(() -> fetchLinkedAccounts(id));
Future<DemographicData> userDetailsFuture = scope.fork(() -> fetchUserDetails(id));
scope.join(); // Join all subtasks
scope.throwIfFailed(e -> new WebApplicationException(e));
//The subtasks have completed by now so process the result
return new Response(accountDetailsFuture.resultNow(),
linkedAccountsFuture.resultNow(),
userDetailsFuture.resultNow());
}
翻译自:Java 21 Features: Practical Examples and Insights