Java 22 新特性 (EOL)
生命周期结束时间(EOL): 2024/09/20
官方发布介绍页面:https://openjdk.org/projects/jdk/22/
1. G1引入区域固定机制,降低GC延迟 (预览)
JEP 423 解决了 Java 与 C 和 C++ 等非托管语言之间的互操作性问题。JNI,作为互操作性的关键组件,允许 Java 应用程序调用其他语言编写的原生应用程序和库,或反过来。它提供了获取和释放 Java 对象指针的函数,这些函数必须成对使用。在获取和释放指针之间执行的代码被视为关键区域,在此期间,JVM 必须确保 GC 不移动关联的 Java 对象。默认的 GC,即 G1,在这些关键区域中会完全禁用垃圾回收,导致严重的延迟。这种方法通常会导致线程停滞、不必要的内存耗尽,甚至在极端情况下会导致 VM 过早关闭。
G1 引入区域固定从根本上改变了这种方式。JEP 423 允许 G1 在 GC 期间固定包含关键对象的特定内存区域,而不是禁用 GC。这是通过在每个区域中维护关键对象的计数来实现的,在获取关键对象时增加计数,在释放时减少。当计数非零时,该区域被视为被固定,不会在 GC 期间被清理。这确保了关键对象不会被移动,避免了禁用 GC,并显著降低了延迟。
这个新特性是 Java 垃圾回收机制的一项重大进步。它旨在解决延迟问题,并在没有活动的 JNI 关键区域存在时保证 GC 停顿时间不出现回归。即使存在这样的区域,回归也是最小的。在 Major GC 和 Minor GC 期间实现区域固定证明了这种解决方案的健壮性。
不过,JEP 423 也存在潜在的风险和假设。一个重大风险是,如果应用程序同时固定多个区域,可能会导致堆内存耗尽。虽然目前没有直接解决方案,但从已经实现了类似机制的 Shenandoah GC 来看,这种风险是可控的。
总的来说,JEP 423 为 OpenJDK 的 G1 垃圾回收器引入了重大的技术增强。区域固定特性解决了与 Java 本地接口(JNI)关键区域相关的延迟问题,改进了 JVM 在与非托管语言发生互操作时的垃圾回收处理。这表明了 OpenJDK 在努力完善和优化 Java 性能和互操作性能力方面所做的持续不断的努力。
2. super(...)之前的语句(预览)
传统上,要求 Java 构造函数将对另一个构造函数的显式调用作为第一条语句。这个约束确保了自上而下的执行顺序,并防止对未初始化字段的访问,极大地限制了构造函数逻辑的表达性和可读性。考虑下面的例子:
public class PositiveBigInteger extends BigInteger {
public PositiveBigInteger(long value) {
super(value); // Potentially unnecessary work
if (value <= 0)
throw new IllegalArgumentException("non-positive value");
}
}
通过在调用超类构造函数之前验证其参数来声明快速失败的构造函数会更好。JEP 447 放宽了这些限制,允许在显式构造函数调用之前出现不引用正在创建的实例的语句。有了这个功能,上面的代码可以简化为:
public class PositiveBigInteger extends BigInteger {
public PositiveBigInteger(long value) {
if (value <= 0)
throw new IllegalArgumentException("non-positive value");
super(value);
}
}
考虑另一种情况,比如子类构造函数需要为超类构造函数准备参数。以前,由于超类构造函数调用必须作为第一条语句,这就需要使用辅助方法。
public class SubClass extends SuperClass {
public SubClass(Certificate certificate) {
super(prepareByteArray(certificate));
}
private static byte[] prepareByteArray(Certificate certificate) {
// Logic to prepare byte array from certificate
// ...
return byteArray;
}
}
在这个例子中,prepareByteArray 方法在将 Certificate 对象传递给 SuperClass 构造函数之前对它进行处理。有了 JEP 447,这个过程变得更加简洁和直观。
public class SubClass extends SuperClass {
public SubClass(Certificate certificate) {
// Directly include the logic to prepare byte array
PublicKey publicKey = certificate.getPublicKey();
if (publicKey == null) {
throw new IllegalArgumentException("Null certificate");
}
byte[] byteArray = switch (publicKey) {
case RSAPublicKey rsaKey -> rsaKey.getEncoded();
case DSAPublicKey dsaKey -> dsaKey.getEncoded();
default -> throw new UnsupportedOperationException("Unsupported key type");
};
super(byteArray);
}
}
在这个新的例子中,SubClass 的构造函数直接包含处理 Certificate 对象的逻辑。这种直接的方法增强了可读性,减少对使用辅助方法的需求,展示了 JEP 447 在实际应用场景中的好处。
JEP 447 不仅提供了更大的灵活性,还保留了构造函数行为的基本保证,确保子类构造函数不会干扰超类的实例化。这个更新不需要对 Java 虚拟机(JVM)做任何修改,仅依赖 JVM 现有的能力来验证和执行构造函数调用之前的代码。
随着 Java 的不断发展,JEP 447 清楚地表明了 Java 在不断适应现代编程实践。它反映了在引入新特性和保持 Java 生态系统健壮性之间的平衡。对于 Java 开发者来说,这意味着有机会探索更高效的编码实践,同时仍然保持对编程语言的核心原则。
3. 外部函数和内存API
外部函数和内存 API (Foreign Function & Memory API) 允许 Java 程序调用本地代码和访问外部内存。提供了一个纯 Java 应用程序接口,用于替代 JNI(Java Native Interface),以支持直接调用本地函数和操作内存。这有助于提高性能和易用性。
import jdk.incubator.foreign.*;
public class ForeignFunctionMemoryExample {
public static void main(String[] args) {
try (MemorySegment segment = MemorySegment.allocateNative(100)) {
MemoryAccess.setIntAtOffset(segment, 0, 42);
int value = MemoryAccess.getIntAtOffset(segment, 0);
System.out.println(value); // 42
}
}
}
4. 未命名变量和模式
Unnamed Patterns and Variables支持用_来替代没有使用的变量声明 不使用本特性之前:
static boolean isDelayTimeEqualToReverbRoomSize(EffectLoop effectLoop) {
if (effectLoop instanceof EffectLoop(Delay(int timeInMs), Reverb(String name, int roomSize))) {
return timeInMs == roomSize;
}
return false;
}
使用本特性以后:
static boolean isDelayTimeEqualToReverbRoomSize(EffectLoop effectLoop) {
if (effectLoop instanceof EffectLoop(Delay(int timeInMs), Reverb(_, int roomSize))) {
return timeInMs == roomSize;
}
return false;
}
int guitarCount = 0;
for (Guitar _ : guitars) {
if (guitarCount < LIMIT) {
guitarCount++;
}
}
var lesPaul = new Guitar("Les Paul");
try {
cart.add(stock.get(lesPaul, guitarCount));
} catch (OutOfStockException _) {
System.out.println("Sorry, out of stock!");
}
所有未使用的变量都可以用_来替代,这样可以提高代码的可读性。
5. 类文件API(预览)
JEP 457 提供了一个用于解析、生成和转换 Java 类文件的标准 API。此 API 处于预览状态。Java 生态系统有许多用于解析和生成 Java 类文件的库:ASM、BCEL、Javassist等大多数字节码生成框架都使用它们。Java类格式每6个月演化一次,每个新的Java版本都会发布,因此生成框架必须同时演进,否则可能会不支持最新的语言演进。JDK 本身使用ASM来实现它的一些工具,以及lambda实现,这在需要生成类文件的部分中,一个Java版本的功能与JVM中可以使用的功能之间存在差异,因为您必须等待支持版本 N 的新功能的 ASM 版本,然后才能在版本N+1中使用它们。类文件API通过在JDK中提供用于解析、生成和转换类文件的API来克服这个问题。
比如要生成如下方法:
void fooBar(boolean z, int x) {
if (z)
foo(x);
else
bar(x);
}
用ASM的示例:
ClassWriter classWriter = ...;
MethodVisitor mv = classWriter.visitMethod(0, "fooBar", "(ZI)V", null, null);
mv.visitCode();
mv.visitVarInsn(ILOAD, 1);
Label label1 = new Label();
mv.visitJumpInsn(IFEQ, label1);
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ILOAD, 2);
mv.visitMethodInsn(INVOKEVIRTUAL, "Foo", "foo", "(I)V", false);
Label label2 = new Label();
mv.visitJumpInsn(GOTO, label2);
mv.visitLabel(label1);
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ILOAD, 2);
mv.visitMethodInsn(INVOKEVIRTUAL, "Foo", "bar", "(I)V", false);
mv.visitLabel(label2);
mv.visitInsn(RETURN);
mv.visitEnd();
使用此特性的示例:
ClassBuilder classBuilder = ...;
classBuilder.withMethod("fooBar", MethodTypeDesc.of(CD_void, CD_boolean, CD_int), flags,
methodBuilder -> methodBuilder.withCode(codeBuilder -> {
Label label1 = codeBuilder.newLabel();
Label label2 = codeBuilder.newLabel();
codeBuilder.iload(1)
.ifeq(label1)
.aload(0)
.iload(2)
.invokevirtual(ClassDesc.of("Foo"), "foo", MethodTypeDesc.of(CD_void, CD_int))
.goto_(label2)
.labelBinding(label1)
.aload(0)
.iload(2)
.invokevirtual(ClassDesc.of("Foo"), "bar", MethodTypeDesc.of(CD_void, CD_int))
.labelBinding(label2);
.return_();
});
6. 启动多文件源码程序
JDK11的JEP 330: Launch Single-File Source-Code Programs引入了直接运行单个源文件的能力,一个缺点是所有源代码都必须放在一个文件中。要处理多个文件,仍然需要单独的编译步骤,这迫使开发人员在其项目中使用构建工具。如果开发人员仍然是初学者,那么情况可能会更糟:他们必须暂停对语言的学习,并且必须优先考虑学习或构建 Maven 或 Gradle 等工具。
Java22引入了一个启动器,它支持运行由多个文件组成的程序。假设一个目录包含两个文件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文件然后内存编译。
如果是引用了jar包的类,则可以通过--class-path lib/*来指定,比如:
Prog1.java
Prog2.java
Helper.java
libs/
├─ library1.jar
├─ library2.jar
可以通过如下来启动
$ java --class-path 'lib/*' Prog1.java
$ java --class-path 'lib/*' Prog2.java
如果libs里头的是模块化的,则可以使用如下方式启动
$ java -p lib Prog1.java
$ java -p lib Prog2.java
7. 字符串模板(第二次预览)
JDK21的JEP 430: String Templates (Preview)引入了String Templates
String title = "My Online Guitar Store";
String text = "Buy your next Les Paul here!";
String html = STR."""
<html>
<head>
<title>\{title}</title>
</head>
<body>
<p>\{text}</p>
</body>
</html>
""";
8. Vector API (第七次孵化器)
- JDK16引入了JEP 338: Vector API (Incubator)提供了jdk.incubator.vector来用于矢量计算
- JDK17进行改进并作为第二轮的incubator JEP 414: Vector API (Second Incubator)
- JDK18的JEP 417: Vector API (Third Incubator)进行改进并作为第三轮的incubator
- JDK19的JEP 426:Vector API (Fourth Incubator)作为第四轮的incubator
- JDK20的JEP 438: Vector API (Fifth Incubator)作为第五轮的incubator
- JDK21的JEP 448: Vector API (Sixth Incubator)作为第六轮的incubator
Vector API 允许在 Java 中进行矢量计算,JDK22则作为第七轮的incubator,使用示例如下
void scalarComputation(float[] a, float[] b, float[] c) {
for (int i = 0; i < a.length; i++) {
c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
}
}
static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED;
void vectorComputation(float[] a, float[] b, float[] c) {
int i = 0;
int upperBound = SPECIES.loopBound(a.length);
for (; i < upperBound; i += SPECIES.length()) {
// FloatVector va, vb, vc;
var va = FloatVector.fromArray(SPECIES, a, i);
var vb = FloatVector.fromArray(SPECIES, b, i);
var vc = va.mul(va)
.add(vb.mul(vb))
.neg();
vc.intoArray(c, i);
}
for (; i < a.length; i++) {
c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
}
}
9. 流收集器 (第一次预览)
流收集器 (Stream Gatherers) 这是一种操作数据流的新机制。Stream Gatherers 允许开发人员创建自定义的中间操作符,以简化复杂的操作。
在 Stream API 中,流从事件源开始,像 filter 和 map 这样的操作被称为“中间”操作。每个中间操作都返回流,因此可以将它们组合在一起。但是使用 Stream API, Java在流到达“终端”操作之前不会开始应用任何这些操作。即使许多操作符链接在一起,这也支持高效的处理。
Stream 内置的中间操作符很强大,但它们无法覆盖所有可想象的需求。对于开箱即用的情况,我们需要一种定义自定义操作的方法。而 Stream Gatherers 给了我们这种方式。
比如创建一个你看到的所有偶数的数组,你可以使用内置的 filter 方法:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
numbers.stream().filter(number -> number % 2 == 0).toArray()
// result: { 2, 4, 6 }
在上面的示例中,我们从一个整数数组开始,然后将其转换为一个流,接着应用一个过滤器,该过滤器只返回除 2 后无余数的数字。
gatherers 接口提供了一些内置函数,能够构建自定义的中间操作。让我们来看看每一个都是做什么的:
-
windowFixed
如果你想把所有飘过的叶子都收集起来,每两桶装一桶呢? 对于内置函数操作符来说,操作起来是非常笨拙的。它需要将一个由个位数组成的数组转换为由数组组成的数组。
而 windowFixed 方法能通过一个简单的方法来将你的叶子收集到桶里:
Stream.iterate(0, i -> i + 1).gather(Gatherers.windowFixed(2)).limit(5).collect(Collectors.toList());上面代码将每两个元素转换成一个新的数组。最后,将流转换为 List:
[[0, 1], [2, 3], [4, 5], [6, 7], [8, 9]]-
windowSliding
另一个函数是 windowSlide,它的工作原理类似于 windowFixed(),只是每个窗口从源数组中的下一个元素开始,而不是从最后一个窗口的末尾开始。这里有一个例子:
Stream.iterate(0, i -> i + 1).gather(Gatherers.windowSliding(2)).limit(5).collect(Collectors.toList());输出:
[[0, 1], [1, 2], [2, 3], [3, 4], [4, 5]]将 windowSliding 的输出与 windowFixed 输出进行比较,将看到差异:与windowFixed 不同,windowSliding 中的每个子数组包含前一子数组的最后一个元素。
-
-
Gatherers.fold
Gatherers.fold 类似于 Stream.reduce 方法的改进版本。reduce是一种 fold。Reduction 采用 stream 并将其转换为单个值。fold 也可以做到这一点,但它放宽了要求:
(1).返回类型与流元素属于同一类型;
(2).合路器是缔合的;
(3).折叠上的初始值设定项是一个实际的生成器函数,而不是静态值。
第二个要求与并行化有关。在 stream 上调用 Stream.parallel 意味着 JVM 可以将worker 分解为多个线程。
Stream.of("hello","world","how","are","you?").gather( Gatherers.fold(() -> "", (acc, element) -> acc.isEmpty()? element : acc + "," + element ) ).findFirst().get();这个例子接受字符串集合,并用逗号将它们组合起来。与 reduce 所做的事情相同:
String result = Stream.of("hello", "world", "how", "are", "you?").reduce("", (acc, element) -> acc.isEmpty() ? element : acc + "," + element);可以看到,使用 fold,定义了一个函数(()-> " "),而不是初始值(" ")。这意味着如果需要对 initiator 进行更复杂的处理,可以使用闭包函数。
现在让我们考虑一下 fold 在类型多样性方面的优势。假设我们有一个混合对象类型的流,我们想要计算出现次数:
var result = Stream.of(1,"hello", true).gather(Gatherers.fold(() -> 0, (acc, el) -> acc + 1));// result.findFirst().get() = 3结果 result 为 3。请注意,流有一个数字、一个字符串和一个布尔值。使用 reduce 执行类似的事情很困难,因为累加器参数 (acc) 是强类型的:
var result = Stream.of(1, "hello", true).reduce(0, (acc, el) -> acc + 1);// 抛出异常:bad operand types for binary operator '+'我们可以使用 collector 来实现这项工作:
var result2 = Stream.of("apple", "banana", "apple", "orange").collect(Collectors.toMap(word -> word, word -> 1, Integer::sum, HashMap::new)); -
Gatherers.scan
Scan 类似于 windowFixed,但它将元素累积到单个元素中,而不是数组中。同样,下面这个示例会为你提供了更清晰的解释:
Stream.of(1,2,3,4,5,6,7,8,9).gather( Gatherers.scan(() -> "", (string, number) -> string + number) ) .toList();输出:
["1", "12", "123", "1234", "12345", "123456", "1234567", "12345678", "123456789"]因此,scan 让我们可以在流元素中移动并将它们累积组合起来。
-
mapConcurrent
使用 mapConcurrent,可以指定在运行提供的 map 函数时要并发使用的最大线程数。这里有一个简单的例子,在对数字进行平方时将并发限制为四个线程:
Stream.of(1,2,3,4,5).gather(Gatherers.mapConcurrent(4, x -> x * x)).collect(Collectors.toList());// 输出: [1, 4, 9, 16, 25]除了制定最大线程数之外,mapConcurrent 的工作方式与标准映射函数完全相同。
10. 结构化并发 (第二次预览)
- JDK19的JEP 428: Structured Concurrency (Incubator)作为第一次incubator
- JDK20的JEP 437: Structured Concurrency (Second Incubator)作为第二次incubator
- JDK21的JEP 453: Structured Concurrency (Preview)作为首次preview
结构化并发旨在简化多线程编程,帮助避免常见的并发问题。JDK22作为第二次预览,使用示例如下:
Response handle() throws ExecutionException, InterruptedException {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Supplier<String> user = scope.fork(() -> findUser());
Supplier<Integer> order = scope.fork(() -> fetchOrder());
scope.join() // Join both subtasks
.throwIfFailed(); // ... and propagate errors
// Here, both subtasks have succeeded, so compose their results
return new Response(user.get(), order.get());
}
}
11. 隐式声明类和实例主方法 (第二次预览)
JDK21的JEP 445: Unnamed Classes and Instance Main Methods (Preview)作为首次preview,引入了未命名的类和实例main方法特性可以简化hello world示例,方便java新手入门
JDK22作为第二次preview,它支持了更为灵活的程序启动协议,比如允许启动类的方法有public、protected权限,如果启动类包含参数方法则会选择有参数的,否则选择无参的main方法。
static void main(String[] args) {
System.out.println("static main with args");
}
static void main() {
System.out.println("static main without args");
}
void main(String[] args) {
System.out.println("main with args");
}
void main() {
System.out.println("main with without args");
}
javac --release 21 --enable-preview Main.javajava --enable-preview Main
其中main方法选择的优先顺序是static的优于非static的,然后有args的优于没有args的
12. 作用域值(第二次预览)
- JDK20的JEP 429: Scoped Values (Incubator)作为Incubator
- JDK21的JEP 446: Scoped Values (Preview)作为preview版本
Scoped Values 提供了一种在特定范围内共享数据的方式,类似于线程本地变量。JDK22作为第二次preview,具体使用如下:
import jdk.incubator.concurrent.ScopedValue;
public class ScopedValuesExample {
private static final ScopedValue<String> SCOPED_VALUE = ScopedValue.newInstance();
public static void main(String[] args) {
ScopedValue.where(SCOPED_VALUE, "Hello, Scoped Value!")
.run(() -> System.out.println(SCOPED_VALUE.get()));
}
}