就在3月19日,Java22重磅发布。Java22新增了12项增强功能,其中包括七个预览特性和一个孵化器特性,这些功能都显著到足以引起JDK增强提案(JEPs)的关注。它们涵盖了Java语言、其API、性能以及JDK中包含的工具的改进。
真的卷不动了,,前段时间才将项目升级到Java17。。。。
接下来我们看看具体的新特性介绍。。。
image.png
Java语言上的改进
Unnamed Variables & Patterns - JEP 456
匿名变量和模式。当需要但未使用变量声明或嵌套模式时,提高了可读性。这两者都用下划线字符表示。
优化:
比如我们可以在循环中这样使用:
static int count(Iterable orders) {
int total = 0;
for (Order _ : orders) // Unnamed variable
total++;
return total;
}
或者
for (int i = 0, _ = sideEffect(); i < 10; i++) { ... i ... }
或者while
循环:
while (q.size() >= 3) {
var x = q.remove();
var _ = q.remove(); // Unnamed variable
var _ = q.remove(); // Unnamed variable
... new Point(x, 0) ...
}
String s = ...
try {
int i = Integer.parseInt(s);
... i ...
} catch (NumberFormatException _) { // Unnamed variable
System.out.println("Bad number: " + s);
}
多个catch
:
try { ... }
catch (Exception _) { ... } // Unnamed variable
catch (Throwable _) { ... } // Unnamed variable
或者这样使用try...resource
try (var _ = ScopedContext.acquire()) { // Unnamed variable
... no use of acquired resource ...
}
在lamba中我们可以这样使用:
...stream.collect(Collectors.toMap(String::toUpperCase,
_ -> "NODATA")) // Unnamed variable
例如:
switch (ball) {
case RedBall _ -> process(ball); // Unnamed pattern variable
case BlueBall _ -> process(ball); // Unnamed pattern variable
case GreenBall _ -> stopProcessing(); // Unnamed pattern variable
}
或者
switch (box) {
case Box(RedBall _) -> processBox(box); // Unnamed pattern variable
case Box(BlueBall _) -> processBox(box); // Unnamed pattern variable
case Box(GreenBall _) -> stopProcessing(); // Unnamed pattern variable
case Box(var _) -> pickAnotherBox(); // Unnamed pattern variable
}
通过这种改进允许我们省略名称,未命名的模式变量使得基于类型模式的运行时数据探索在switch语句块以及使用instanceof运算符时,视觉上更加清晰明了。
Statements before super - JEP 447
构造器中的前置语句。在构造函数中,允许在显式构造函数调用之前出现不引用正在创建的实例的语句。
优化:
有时我们需要验证传递给超类构造函数的参数。虽然我们可以在事后进行参数验证,但这意味着可能会进行不必要的操作。例如如下:
public class PositiveBigInteger extends BigInteger {
public PositiveBigInteger(long value) {
super(value); // Potentially unnecessary work
if (value ...
};
}
}
超类构造函数接受一个字节数组作为参数,而子类构造函数接受一个Certificate
对象作为参数。为了满足超类构造函数调用必须为子类构造函数中的第一条语句这一限制,我们声明了一个辅助方法prepareByteArray
来为此调用准备参数。
如果能够将参数准备代码直接嵌入到构造函数中,这段代码会更具可读性。在Java22中我们可以这么做:
public Sub(Certificate certificate) {
var publicKey = certificate.getPublicKey();
if (publicKey == null)
throw new IllegalArgumentException("null证书");
final byte[] byteArray = switch (publicKey) {
case RSAKey rsaKey -> ... // RSA密钥转换为字节数组
case DSAPublicKey dsaKey -> ... // DSA公钥转换为字节数组
...
default -> ... // 其他情况处理逻辑
};
super(byteArray);
}
String Templates - JEP 459:
字符串模板。字符串模板通过将文本文字与嵌入表达式和模板处理器相结合,以产生专门的结果,来补充 Java 的现有字符串文字和文本块。
优化:
字符串的模板可以直接在代码中表达,就像注释字符串一样,Java 运行时会自动将特定于模板的规则应用于字符串。从模板编写字符串将使开发人员不必费力地转义每个嵌入表达式、调用validate()
整个字符串或使用java.util.ResourceBundle
来查找本地化字符串。
比如我们可以构造一个表示JSON文档的字符串,然后将其提供给JSON解析器:
String name = "Joan Smith";
String phone = "555-123-4567";
String address = "1 Maple Drive, Anytown";
String json = """
{
"name": "%s",
"phone": "%s",
"address": "%s"
}
""".formatted(name, phone, address);
JSONObject doc = JSON.parse(json);
字符串的 JSON 结构可以直接在代码中表达,Java运行时会JSONObject
自动将字符串转换为。无需通过解析器进行手动绕行。我们使用基于模板的字符串组合机制,我们就可以提高几乎每个Jav 程序的可读性和可靠性。这种功能将提供插值的好处,就像在其他编程语言中看到的那样,但不太容易引入安全漏洞。它还可以减少使用将复杂输入作为字符串的库的繁琐。
我们还可以使用模板STR
处理器,STR
是 Java 平台中定义的模板处理器。它通过将模板中的每个嵌入表达式替换为该表达式的(字符串化)值来执行字符串插值。STR
是public
static
final
自动导入到每个Java源文件中的字段。
// Embedded expressions can be strings
String firstName = "Bill";
String lastName = "Duck";
String fullName = STR."{firstName} {lastName}";
| "Bill Duck"
String sortName = STR."{lastName}, {firstName}";
| "Duck, Bill"
// Embedded expressions can perform arithmetic
int x = 10, y = 20;
String s = STR."{x} + {y} = {x + y}";
| "10 + 20 = 30"
// Embedded expressions can invoke methods and access fields
String s = STR."You have a {getOfferType()} waiting for you!";
| "You have a gift waiting for you!"
String t = STR."Access at {req.date} {req.time} from {req.ipAddress}";
| "Access at 2022-03-25 15:34 from 8.8.8.8"
模板表达式的模板可以跨越多行源代码,使用类似于文本块的语法。
String title = "My Web Page";
String text = "Hello, world";
String html = STR."""
{title}
{text}
""";
| """
|
|
| My Web Page
|
|
| Hello, world
|
|
| """
String name = "Joan Smith";
String phone = "555-123-4567";
String address = "1 Maple Drive, Anytown";
String json = STR."""
{
"name": "{name}",
"phone": "{phone}",
"address": "{address}"
}
""";
| """
| {
| "name": "Joan Smith",
| "phone": "555-123-4567",
| "address": "1 Maple Drive, Anytown"
| }
| """
Implicitly Declared Classes and Instance Main Methods - JEP 463:
隐式声明的类和实例主方法。这项Java增强引入了隐式声明的类以及实例主方法的功能,允许开发人员在不明确显式声明类的情况下编写类结构,并能够在类实例上直接定义和执行类似于传统main
方法的入口点。这一特性旨在简化编程模型,特别是对于初学者和小型脚本场景,使得无需了解大型程序设计所需的完整类声明结构也能快速编写可运行的Java代码。
优化:总体来说可以快速学习Java。
我们以入门Java的第一行代码Hello World
为例:
class HelloWorld {
void main() {
System.out.println("Hello, World!");
}
}
Java22还可以隐式声明一个类:
void main() {
System.out.println("Hello, World!");
}
还可以这样:
String greeting() { return "Hello, World!"; }
void main() {
System.out.println(greeting());
}
或者,使用字段,如:
String greeting = "Hello, World!";
void main() {
System.out.println(greeting);
}
Java API库上的改进
Foreign Function & Memory API - JEP 454:
外部函数和内存API。允许Java程序与Java运行时之外的代码和数据进行交互。通过高效地调用外部函数(即JVM外部的代码)和安全地访问外部内存(即JVM不管理的内存),该API使Java程序能够调用本地库并处理本地数据,而无需JNI的脆弱性和危险性。
优化
Java22提供外部函数和内存API(FFM API)定义类和接口,以便开发者使用他们可以
- 控制外部内存(
MemorySegment
、Arena
和SegmentAllocator
)的分配和释放, - 操作和访问结构化的外部存储器
MemoryLayout
和VarHandle
- 调用外部函数(
Linker
、SymbolLookup
、FunctionDescriptor
和MethodHandle
)。
FFM API在java.lang.foreign
包中。
Class-File API - JEP 457:
类文件API。提供了用于解析、生成和转换 Java 类文件的标准 API。
优化:
Java22为 Class-File API 采用了以下设计目标和原则。
- 类文件实体由不可变对象表示所有类文件 实体,例如字段、方法、属性、字节码指令、注释等, 由不可变对象表示。这有利于在以下情况下进行可靠共享 正在转换类文件。
- 树结构表示类文件具有树结构。一个类 有一些元数据(名称、超类等)和可变数量的字段, 方法和属性。字段和方法本身具有元数据,并进一步 包含属性,包括属性。属性 further 包含指令、异常处理程序等。用于 导航和生成类文件应反映此结构。CodeCode
- 用户驱动的导航我们通过类文件树的路径是 由用户选择驱动。如果用户只关心字段上的注释,那么 我们只需要解析结构内部的注释属性;我们不应该研究任何一个类 属性或方法的主体,或字段的其他属性。用户 应该能够处理复合实体,例如方法,无论是作为单个实体 单位或作为其组成部分的流,根据需要。field_info
- 懒惰用户驱动的导航可显著提高效率,例如 不解析超过满足用户要求的类文件 需要。如果用户不打算深入研究方法的内容,那么我们 不需要解析比需要更多的结构 下一个类文件元素开始的位置。我们可以懒洋洋地膨胀和缓存, 用户请求时的完整表示形式。method_info
- 统一的流式处理和具体化视图与 ASM 一样,我们希望同时支持两者 类文件的流式处理和实例化视图。流视图是 适用于大多数用例,而物化视图更 一般,因为它支持随机访问。我们可以提供一个物化的观点 通过懒惰比 ASM 便宜,这是由不变性实现的。我们可以, 此外,对齐流式视图和实例化视图,以便它们使用通用的 词汇表,可以协调使用,因为每个用例都很方便。
- 紧急转换如果类文件解析和生成 API 是 充分对齐,那么转换可以是一个紧急属性,可以 不需要自己的特殊模式或重要的新 API 图面。(ASM实现 这是通过为读者和作者使用通用的访客结构来实现的。如果类, 字段、方法和代码体是可读和可写的,作为 元素,则可以将转换视为对此的平面映射操作 流,由 lambda 定义。
- 细节隐藏类文件的许多部分(常量池、引导方法 表、堆栈图等)派生自类文件的其他部分。它 要求用户直接构建这些是没有意义的;这是额外的工作 对于用户来说,并增加了出错的机会。API 将自动 生成与其他实体紧密耦合的实体 添加到类文件中的字段、方法和指令。
Class-File API
在 java.lang.classfile
包和子包中。 它定义了三个主要抽象:
- 元素是对类文件某部分的一种不可变描述,可能是一个指令、属性、字段、方法,甚至是整个类文件。有些元素,如方法,是复合元素;除了本身是元素外,它们还包含自身的元素,可以作为一个整体处理,也可以进一步分解。
- 每种类型的复合元素都有一个对应的构建器,该构建器拥有特定的构建方法(例如,ClassBuilder::withMethod),并且也是相应元素类型的消费者。
- 最后,变换代表了一个函数,它接收一个元素和一个构建器,并调解如何(如果有的话)将该元素转换为其他元素。
Stream Gatherers - JEP 461:
流收集器。增强了 Stream API,以支持自定义中间操作。这将允许流管道以不易通过现有内置中间操作实现的方式转换数据。
优化:
- 使流管道更加灵活和富有表现力。
- 尽可能允许自定义中间操作来操作无限大小的流。
流(Stream)::gather(Gatherer) 是一种新的中间流操作,通过应用用户自定义实体——收集器(Gatherer)来处理流中的元素。利用gather操作,我们可以构建高效且适用于并行处理的流,实现几乎所有的中间操作。Stream::gather(Gatherer) 在中间操作中的作用类似于Stream::collect(Collector)在终止操作中的作用。
Gatherer用于表示对流中元素的转换,它是java.util.stream.Gatherer接口的一个实例。Gatherer可以以一对一、一对多、多对一或多对多的方式转换元素。它可以跟踪已处理过的元素以影响后续元素的转换,支持短路操作以将无限流转换为有限流,并能启用并行执行。例如,一个Gatherer可以从输入流中按条件转换一个输入元素为一个输出元素,直到某一条件变为真,此时开始将一个输入元素转换为两个输出元素。
Gatherer由四个协同工作的函数定义:
当调用Stream::gather时,执行以下等效步骤:
现有Stream接口中声明的所有中间操作都可以通过调用带有实现该操作的Gatherer的gather方法来实现。例如,对于一个T类型元素的流,Stream::map通过应用一个函数将每个T元素转换为U元素并将其向下传递;这实质上就是一个无状态的一对一Gatherer。另一个例子是Stream::filter,它采用一个谓词决定输入元素是否应向下传递;这只是一个无状态的一对多Gatherer。事实上,从概念上讲,每一个流管道都可以等同于:
source.gather(...).gather(...).gather(...).collect(...)
Structured Concurrency - JEP 462:
结构化并发。简化了并发编程。结构化并发将在不同线程中运行的相关任务组视为单个工作单元,从而简化了错误处理和取消,提高了可靠性并增强了可观察性。
优化:
结构化并发API的主要类是java.util.concurrent
包中的StructuredTaskScope
类。此类允许开发人员将任务结构化为一组并发子任务,并将它们作为一个整体进行协调管理。子任务通过分别创建新线程(fork)并在之后作为一个整体等待它们完成(join)和可能的情况下作为一个整体取消它们。子任务的成功结果或异常将被父任务聚合并处理。StructuredTaskScope
确保了子任务的生命周期被限定在一个清晰的词法作用域内,在这个作用域内,任务与其子任务的所有交互,包括分叉(fork
)、同步(join
)、取消(cancellation
)、错误处理和结果合成都在此发生。
使用StructuredTaskScopes实例:
Response handle() throws ExecutionException, InterruptedException {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Supplier user = scope.fork(() -> findUser());
Supplier order = scope.fork(() -> fetchOrder());
scope.join() // 同步两个子任务
.throwIfFailed(); // 并传播错误信息
// 这里,两个子任务都已经成功,所以组合它们的结果
return new Response(user.get(), order.get());
}
}
这里理解涉及线程的生命周期变得简单:在所有情况下,它们的生命周期都被限制在一个词法作用域内,即try-with-resources
语句的主体内。此外,使用StructuredTaskScope
确保了一系列有价值的特性:
findUser()
或fetchOrder()
子任务之一失败,尚未完成的另一个子任务将被取消。(这是由ShutdownOnFailure
实现的关闭策略管理的,也有可能实现其他策略)。join()
之前或期间执行handle()
方法的线程被中断,则当线程退出作用域时,两个子任务都将自动取消。findUser()
和fetchOrder()
的线程在转储中显示为scope的子线程。Scoped Values - JEP 464:
作用域值优化。在线程内和跨线程之间有效共享不可变数据。这个Java增强它旨在提供一种机制,允许开发者在Java应用程序中安全地在线程内部以及跨线程之间共享不可变数据。该特性旨在替代或改善现有的ThreadLocal机制,提供一种更加可控、易于管理和高效的解决方案,尤其是在涉及大规模并发处理和跨层数据传递场景时。通过范围值,开发人员可以更好地组织和管理在特定作用域内有效的变量,同时确保资源的有效利用和数据的安全共享。
优化:
- 易用性 — 理解数据流应当轻松直观。
- 可理解性 — 共享数据的生命周期可以从代码的语法结构中清晰可见。
- 健壮性 — 调用者共享的数据只能被合法的被调用者获取。
- 性能 — 数据能够有效地在大量线程间高效共享。
作用域值是一种容器对象,允许方法在同一个线程内安全高效地将其数据值与直接和间接的被调用者共享,同时也能与子线程共享,而无需依赖方法参数。它是一个类型为ScopedValue
的变量,通常被声明为final static
字段,并设置为private
访问权限,以防止其他类的代码直接访问。
类似线程局部变量,作用域值关联了多个值,每个线程对应一个。具体使用的值取决于哪个线程调用了它的方法。不同于线程局部变量,范围限定值只被写入一次,并且在线程执行期间只能在一定时间段内可用。
作用域值的使用如下所示。一些代码调用ScopedValue.where
,提供一个范围限定值及其要绑定的对象。调用run方法会绑定该范围限定值,为当前线程提供一个特定的副本,然后执行作为参数传递的lambda
表达式。在run方法执行期间,lambda
表达式或从中直接或间接调用的任何方法,都可以通过值的get()
方法读取范围限定值。当run方法执行完毕后,该绑定关系会被销毁。
final static ScopedValue NAME = ScopedValue.newInstance();
// 在某个方法中
ScopedValue.where(NAME, )
.run(() -> { ... NAME.get() ... 调用方法 ... });
// 在lambda表达式中直接或间接调用的方法中
... NAME.get() ...
代码的结构明确了线程可以读取其作用域值副本的时间段。这个有限的生命周期极大地简化了对线程行为的推理。数据从调用者单向传输至直接和间接的被调用者,一眼就能看出。不存在能让远端代码随时改变范围限定值的set方法。这也有助于提高性能:无论调用者和被调用者的栈距离如何,通过get()方法读取作用域值的速度常常与读取局部变量一样快。
Vector API - JEP 460:
矢量API。一个能够在支持的CPU架构上运行时可靠编译为最优矢量指令的API,从而实现优于等效标量计算的性能。本JEP提议在JDK 22中重新孵化该API,相比于JDK 21版本,API进行了些许增强。实现内容包括bug修复和性能优化。主要变更如下:支持通过任意原始元素类型的数组支持的堆内存MemorySegments
进行矢量访问。在此之前,访问仅限于由字节数组支持的堆内存MemorySegments
。
优化:
Project Valhalla
项目的契合Vector API
的长期目标是利用Project Valhalla
对Java对象模型的增强功能。主要来说,这意味着将Vector API
当前基于值的类更改为值类,以便程序能够处理值对象,即缺乏对象标识性的类实例。因此,Vector API
将在多个版本中孵化,直至Project Valhalla
的必要特性作为预览功能可用。一旦这些Valhalla特性可用,我们将调整Vector API及其实现以使用这些特性,并将Vector API
本身提升为预览功能。向量由抽象类Vector
表示。类型变量E被实例化为矢量覆盖的标量基本整数或浮点元素类型的装箱类型。一个向量还具有形状属性,该属性定义了矢量的大小(以位为单位)。当矢量计算由HotSpot C2
编译器编译时,向量的形状决定了Vector
实例如何映射到硬件矢量寄存器。向量的长度,即车道数或元素个数,等于矢量大小除以元素大小。
支持的一系列元素类型(E)包括Byte、Short、Integer、Long、Float和Double,分别对应于标量基本类型byte、short、int、long、float和double。
支持的一系列形状对应于64位、128位、256位和512位的矢量大小,以及最大位数。512位形状可以将字节打包成64个车道,或者将整数打包成16个车道,具有这种形状的矢量可以一次性操作64个字节或16个整数。max-bits形状支持当前架构的最大矢量尺寸,这使得API能够支持ARM SVE
平台,该平台实现可以支持从128位到2048位,以128位为增量的任何固定尺寸。
性能上的改进
Regional Pinning for G1 - JEP 423:
区域固定。通过在G1中实现区域固定(regional pinning),从而在Java Native Interface (JNI) 临界区域内不需要禁用垃圾收集,以此来减少延迟。
优化:
工具类
Launch Multi-File Source-Code Programs - JEP 458:
启动多文件源代码程序。允许用户在不首先编译程序的情况下运行由多个 Java 源代码文件提供的程序。
优化:
Java22增强了Java启动器的源文件模式,使其能够运行以多份Java源代码文件形式提供的程序。
举例来说,假设一个目录包含了两个文件:Prog.java和Helper.java,每个文件各声明一个类:
// Prog.java
class Prog {
public static void main(String[] args) { Helper.run(); }
}
// Helper.java
class Helper {
static void run() { System.out.println("Hello!"); }
}
运行命令java Prog.java
将会在内存中编译Prog类并调用其main方法。由于Prog类中的代码引用了Helper类,启动器会在文件系统中查找Helper.java文件,并在内存中编译Helper类。如果Helper类中的代码又引用了其他类,例如HelperAux类,那么启动器还会找到HelperAux.java并对其进行编译。
当不同.java文件中的类互相引用时,Java启动器并不保证按照特定顺序或时间点编译这些.java文件。例如,启动器可能先编译Helper.java再编译Prog.java。有些代码可能在程序开始执行前就已经被编译,而其他代码可能在需要时才懒加载编译。
只有被程序引用到的类所在的.java文件才会被编译。这样一来,开发者可以在尝试新版本代码时不必担心旧版本会被意外编译。例如,假设目录中还包含OldProg.java文件,其中包含Progr类的一个旧版本,该版本期望Helper类有一个名为go的方法而不是run方法。当运行Prog.java时,存在包含潜在错误的OldProg.java文件并不会影响程序执行。
一个.java文件中可以声明多个类,且会被一起编译。在一个.java文件中共声明的类优先于在其他.java文件中声明的类。例如,假设上面的Prog.java文件扩展后也在其中声明了Helper类,尽管Helper.java文件中已有一个同名类。当Prog.java中的代码引用Helper时,会使用在Prog.java中共同声明的那个类;启动器不会去搜索Helper.java文件。
源代码程序中禁止重复的类声明。也就是说,同一个.java文件内或构成程序的不同.java文件之间的类声明,如果名称相同,则不允许存在。假设经过编辑后,Prog.java和Helper.java最终变成以下形式,其中类Aux意外地在两个文件中都被声明:
// Prog.java
class Prog {
public static void main(String[] args) { Helper.run(); Aux.cleanup(); }
}
class Aux {
static void cleanup() { ... }
}
// Helper.java
class Helper {
static void run() { ... }
}
class Aux {
static void cleanup() { ... }
}
运行命令java Prog.java
会编译Prog.java中的Prog和Aux类,调用Prog类的main方法,然后——由于main方法引用了Helper——查找并编译Helper.java中的Helper和Aux类。Helper.java中对Aux类的重复声明是不允许的,所以程序会停止运行,启动器报告错误。
当通过Java启动器传递单个.java文件名称时,就会触发其源文件模式。如果提供了额外的文件名,它们会成为主方法的参数。例如,运行命令java Prog.java Helper.java
会导致字符串数组"Helper.java"作为参数传给Prog类的main方法。
其他特性
除了JEP中描述的更改之外,发行说明中还列出了许多较小的更新,这些更新对许多应用程序开发者有重要意义。其中包括废弃过时的API和移除先前已经弃用的API。
最后,JDK 22是通过六个月的发布节奏按时交付的13th功能版本。由于预期改进源源不断,这种程度的可预测性使开发人员能够轻松管理创新的采用。Oracle 不会为 JDK 22 提供长期支持,在 2023 年 9 月之前提供更新,之后它将被 Oracle JDK 23 取代。最近的长期维护版本是Java 21。