Java 24 新特性学习
Java 24 也是一个过渡版本, 还是到下面两个链接中找相应的更新
IntelliJ IDEA 对 Java 24 Language level 描述是
- 24 - No new language features
- 24 (Preview) - Flexible constructor bodies, simple source files, etc.
把上面第二个链接中的特性列出来
- 404: Generational Shenandoah (Experimental)
- 450: Compact Object Headers (Experimental)
- 472: Prepare to Restrict the Use of JNI
- 475: Late Barrier Expansion for G1
- 478: Key Derivation Function API (Preview)
- 479: Remove the Windows 32-bit x86 Port
- 483: Ahead-of-Time Class Loading & Linking
- 484: Class-File API *
- 485: Stream Gatherers *
- 486: Permanently Disable the Security Manager
- 487: Scoped Values (Fourth Preview)
- 488: Primitive Types in Patterns, instanceof, and switch (Second Preview)
- 489: Vector API (Ninth Incubator)
- 490: ZGC: Remove the Non-Generational Mode
- 491: Synchronize Virtual Threads without Pinning
- 492: Flexible Constructor Bodies (Third Preview)
- 493: Linking Run-Time Images without JMODs
- 494: Module Import Declarations (Second Preview)
- 495: Simple Source Files and Instance Main Methods (Fourth Preview)
- 496: Quantum-Resistant Module-Lattice-Based Key Encapsulation Mechanism
- 497: Quantum-Resistant Module-Lattice-Based Digital Signature Algorithm
- 498: Warn upon Use of Memory-Access Methods in sun.misc.Unsafe
- 499: Structured Concurrency (Fourth Preview)
- 501: Deprecate the 32-bit x86 Port for Removal
Java 24 中列出的特性恰如其版本一样,有 24 条,因其是一个过渡版本,多数为非正式特性,也就为什么 IntelliJ IDEA 从语言特性上把 Java 标记为 No new language features. 其实也不然,个人觉得有两个正式特性值得关注,即 Class-File API 和 Stream Gathers。
下面拣几个重点描述,有些在前面的版本尚处于 Incubator 或 Preview 状态时提到,会给出相应的链接,还有些不甚重要的只一笔带过.
首先两个不需要太多解释的小特性
JEP 485: Stream Gatherers
关于 Stream Gatherers 的介绍,可参考 Java 22 新特性学习 - JEP 461: Stream Gatherers (Preview).
这里仍然是要理解引入 Stream Gatherers 的意图,Stream 通过 reduce/collect 方法进行终结操作(terminal operation), 由接口 java.util.stream.Collector
可自定义 Collector. 而相应的,java.util.stream.Gatherer 是为 Stream 的中间方法(intermediate operation) 定义的接口,这样对 Stream
的中间操作就不局限于 map, filter 等,可以是统一的
1stream.gather(Gatherer 实现).gather(Gatherer 实现).collect(Collector);
有了 Gatherer 接口,Java 也就不需要像 Java 9 那样给 Stream 特别的添加 takeWhile(), dropWhile() 等方法,以后只需 Gatherer
接口来扩展 Stream 的中间方法了。
java.util.stream.Gatherer 的接口定义,或理解其内部工作过程并不容易,即使用 Gatherer.of() 和 Gatherer.ofSequential() 工厂方法
创建一个 Gatherer 实例也有着使用 Stream.reduce() 一样的复杂性,所以我主要关心的是基于该接口有哪些第三方的库可用。
找到两个第三方 Gatherer 实现库
这两个库提供的 Gatherer 实现已经够丰富了,像 distinctBy, minBy, shuffle, mapUntil, nCopies, repeat, zip,
mapWithIndex, windowSlidinWithIndex, windowFixedWithIndex; foldIndexed, throttle, takeEveryNth, groupBy,
window, movingMedian(window), movingProduct(window).
下面是一个使用了 Packrat 的 distinctBy 根据某个字段值去重的例子
1record Person(String name, String address);
2
3Person p1, p2, p3;
4List<Person> persons = Stream.of(p1, p2, p3).gather(Packrat.distinctBy(Person::name)).toList();
估计后面两大通用组件 Guava 和 Apache Common 也会染指 Gatherer, 会提供更多的 Gatherer 实现。
JEP 484: Class-File API
Class-File API 经过 Java 22, 23 两次 Preview 便在 Java 24 中转为正式特性。一般来说在我们的代码中也不会直接用 Class-File API, 但它会影响一些 AOP, 或 Mock 测试框架的实现,Class-File API 为解析,生成,和转换(修改) Java class(字节码) 提供了标准 API, 它的目的不是为 了替代 ASM 库,如 ASM, BCEL, Javassist , 但足以改变第三方 ASM 库的生态。前面一直未真正了解过 Class-File API, 这里简单学习一下。
由于 Java Class 文件格式不断在演进,如支持 sealed classes, dynamic constants 和 nestmates 等,所以会对第三方库造成不兼容性,
所以要提供一个标准的 API. 与其阅读 JEP 484: Class-File API, 还不如参考
Class-File API。
解析 Class 文件
假设有下面的 Cat 类生成的 Cat.class 文件
1public class Cat implements Animal {
2 private String name = "Rogue";
3
4 public void makeSound() {
5 System.out.printf("%s meow\n", name);
6 }
7}
它实现了 Animal 接口, 我们用 Class-File API 来解析
1import java.io.IOException;
2import java.lang.classfile.*;
3import java.nio.file.Files;
4import java.nio.file.Path;
5
6public class ParseClass {
7 public static void main(String[] args) throws IOException {
8 ClassModel cm = ClassFile.of().parse(Files.readAllBytes(Path.of("target/classes/Cat.class")));
9 System.out.printf("Version: %d.%d\n", cm.majorVersion(), cm.minorVersion());
10 System.out.printf("Interfaces: %s\n", cm.interfaces());
11 System.out.println("------fields------");
12 for (FieldModel field : cm.fields()) {
13 System.out.println(field);
14 }
15 System.out.println("------methods------");
16 cm.methods().forEach(System.out::println);
17 System.out.println("------class elements------");
18 for(ClassElement ce: cm) {
19 System.out.println(ce);
20 }
21 System.out.println("------makeSound code------");
22 CodeModel meowCodeModel = cm.methods().stream().filter(m -> m.methodName().equalsString("makeSound"))
23 .map(MethodModel::code).findFirst().get().get();
24 for (CodeElement e : meowCodeModel) {
25 System.out.println(e);
26 }
27 }
28}
看起来像是在反射,然而核心 API 都在 java.lang.classfile 包下,如 ClassModel, FieldModel, MethodModel, CodeModel,
CodeElement 等等.
上面代码的输出为
1Version: 68.0
2Interfaces: [7 Animal]
3------fields------
4FieldModel[fieldName=name, fieldType=Ljava/lang/String;, flags=2]
5------methods------
6MethodModel[methodName=<init>, methodType=()V, flags=1]
7MethodModel[methodName=makeSound, methodType=()V, flags=1]
8------class elements------
9AccessFlags[flags=33]
10ClassFileVersion[majorVersion=68, minorVersion=0]
11Superclass[superclassEntry=java/lang/Object]
12Interfaces[interfaces=Animal]
13FieldModel[fieldName=name, fieldType=Ljava/lang/String;, flags=2]
14MethodModel[methodName=<init>, methodType=()V, flags=1]
15MethodModel[methodName=makeSound, methodType=()V, flags=1]
16Attribute[name=SourceFile]
17------makeSound code------
18LocalVariable[name=this, slot=0, type=LCat;]
19Label[context=CodeModel[id=445051633], bci=0]
20LineNumber[line=5]
21Field[OP=GETSTATIC, field=java/lang/System.out:Ljava/io/PrintStream;]
22LoadConstant[OP=LDC, val=%s meow
23]
24UnboundIntrinsicConstantInstruction[op=ICONST_1]
25NewRefArray[OP=ANEWARRAY, type=java/lang/Object]
26UnboundStackInstruction[op=DUP]
27UnboundIntrinsicConstantInstruction[op=ICONST_0]
28Load[OP=ALOAD_0, slot=0]
29Field[OP=GETFIELD, field=Cat.name:Ljava/lang/String;]
30UnboundArrayStoreInstruction[op=AASTORE]
31Invoke[OP=INVOKEVIRTUAL, m=java/io/PrintStream.printf(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;]
32UnboundStackInstruction[op=POP]
33LineNumber[line=6]
34Return[OP=RETURN]
35Label[context=CodeModel[id=445051633], bci=21]
生成新的 Class
比如基于一个接口 Animal 在内存中生成 Dog 类,并加载执行
1import java.lang.classfile.ClassFile;
2import java.lang.constant.ClassDesc;
3import java.lang.constant.ConstantDescs;
4import java.lang.constant.MethodTypeDesc;
5import java.lang.invoke.MethodHandles;
6
7public class GenerateClass {
8 public static void main(String[] args) throws Exception {
9 ClassFile cf = ClassFile.of();
10 byte[] bytes = cf.build(
11 ClassDesc.of("Dog"),
12 clb -> clb
13 .withFlags(ClassFile.ACC_PUBLIC)
14 .withInterfaceSymbols(ClassDesc.of("Animal"))
15
16 // Add a default constructor: public Dog() { super(); }
17 .withMethod(ConstantDescs.INIT_NAME, MethodTypeDesc.of(ConstantDescs.CD_void),
18 ClassFile.ACC_PUBLIC,
19 mb -> mb.withCode(cb -> cb
20 .aload(0)
21 .invokespecial(ConstantDescs.CD_Object, ConstantDescs.INIT_NAME, MethodTypeDesc.of(ConstantDescs.CD_void))
22 .return_()
23 ))
24
25 // Implement the makeSound method: public void makeSound() { System.out.println("Woof!"); }
26 .withMethod(
27 "makeSound",
28 MethodTypeDesc.of(ConstantDescs.CD_void),
29 ClassFile.ACC_PUBLIC,
30 mb -> mb.withCode(cb -> cb
31 .getstatic(ClassDesc.of("java.lang.System"), "out", ClassDesc.of("java.io.PrintStream"))
32 .ldc("Woof!")
33 .invokevirtual(ClassDesc.of("java.io.PrintStream"), "println",
34 MethodTypeDesc.of(ConstantDescs.CD_void, ConstantDescs.CD_String))
35 .return_()
36 )
37 )
38 );
39
40 Class<?> dogClass = MethodHandles.privateLookupIn(Animal.class, MethodHandles.lookup()).defineClass(bytes);
41
42 Animal dog = (Animal) dogClass.getDeclaredConstructors()[0].newInstance();
43 dog.makeSound();
44 }
45}
执行
Woof!
符合预期,也可以用下面自定义类加载器的方式来加载 Dog 类
1class CustomClassLoder extends ClassLoader {
2 public Class<?> load(String name, byte[] b) {
3 return super.defineClass(name, b, 0, b.length);
4 }
5}
6
7Class<?> dogClass = new CustomClassLoder().load("Dog", bytes);
修改已有类
对于当前 ClassLoader 可见的类文件在类加载之前可以对其进行修改
1import java.lang.classfile.ClassFile;
2import java.lang.classfile.ClassTransform;
3import java.lang.classfile.MethodTransform;
4import java.lang.classfile.instruction.ReturnInstruction;
5import java.lang.constant.ClassDesc;
6import java.lang.constant.ConstantDescs;
7import java.lang.constant.MethodTypeDesc;
8import java.nio.file.Files;
9import java.nio.file.Path;
10
11public class TransferClass {
12 public static void main(String[] args) throws Exception {
13 var cf = ClassFile.of();
14 Path classPath = Path.of("target/classes/Cat.class");
15 byte[] original = Files.readAllBytes(classPath);
16
17 ClassTransform ct = ClassTransform.transformingMethods(
18 mm -> !mm.methodName().stringValue().equals("<init>"),
19 MethodTransform.transformingCode(
20 (codeBuilder, elem) -> {
21 if (elem instanceof ReturnInstruction ret) {
22 // 在 return 前注入:System.out.println("method exit")
23 codeBuilder
24 .getstatic(ClassDesc.of("java.lang.System"), "out", ClassDesc.of("java.io.PrintStream"))
25 .ldc("method exit")
26 .invokevirtual(ClassDesc.of("java.io.PrintStream"), "println",
27 MethodTypeDesc.of(ConstantDescs.CD_void, ConstantDescs.CD_String))
28 .with(ret);
29 } else {
30 codeBuilder.with(elem);
31 }
32 }
33 )
34 );
35
36 byte[] instrumented = cf.transformClass(cf.parse(original), ct);
37 Files.write(classPath, instrumented);
38
39 new Cat().makeSound();
40 }
41}
执行上面的代码会对 Class 文件进行重复修改,为避免多个 method exit, 执行前用 mvn clean compile 生成新鲜的 Cat.class 文件。Cat.makeSound()
原本的输出为
Rogue meow
执行 TransferClass 后的输出为
method exit
Rogue meow
这是一个简单的演示代码,所以要确保它能得到预期的结果,必须在 Cat 类加载之前执行,并且为避免 Cat.class 被修重复修改,可在修改完做上标记。
如果 Cat.class 对当前类加载器不可见,可用定制类加载器从内存加载修改后的字节。
Class-File API 的内容在本文占主要篇幅,下面对其他特性大概说一下。
JEP 472: 准备有限的使用 JNI
自 Java 22 起, Foreign Function & Memory (FFM) API 可替代 JNI 的使用,但性能上由于有 JIT, 不 一定比纯 JNI, 或 JNA 效率高。
该 JEP 的进一步解释是保留 JNI 为本地代码的标准交互方式,无论是用 JNI 还是 FFM API 在启动时都有警告信息,开发者将要显式的声明要使用 JNI 或 FFM API. 它并非是不建议使用 JNI 或将从 Java 中移除 JNI, 并不会限制通过 JNI 调用本地代码的行为。好像是说不会对 JNI 有任何限制,那标题怎么说的。
该 JEP 的一个要义是 JNI 很危险,使用需谨慎,它可能会破坏 Java 的诚信约定(integrity by default.
例如 direct byte buffers 让垃圾收集器特别对待。FFM API 作为 JNI 的更优先选择,比 JNI 相对安全.
比如在用 JNA 加载动态库时,会收到如下警告信息
WARNING: A restricted method in java.lang.System has been called
WARNING: java.lang.System::load has been called by com.sun.jna.Native in an unnamed module (file:/Users/yanbin/.m2/repository/net/java/dev/jna/jna/5.18.1/jna-5.18.1.jar)
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled
用纯 JNI 或 FFM API 都会有一样的警告信息,要加上 JVM 参数 --enable-native-access=ALL-UNNAMED 来避免警告信息,或者更细粒度的
--enable-native-access=模块1,模块2 来允许特定模块使用 JNI 或 FFM API.
JEP 404: 分代式 Shenandoah 垃圾收集(实验性的)
Java 中有 G1GC, 和新的 ZGC,还有 Shenandoah GC (Java 15 正式引入该 GC)还在发展,传统 GC 包括 G1 在压缩堆时都必须停止所有应用线程,而 Shenandoah GC
通过使用读屏障和写屏障来实现并发压缩堆,减少停顿时间。Java 24 开始实验性对 Shenandoah GC 加入象 G1, ZGC 普遍应用的将堆划分为年轻代和老年代
的特性,以优化垃圾收集的性能。在 Java 24 要使用 Shenadoah GC 并开启堆分代的话,命令为
1java -XX:+UseShenandoahGC \
2 -XX:+UnlockExperimentalVMOptions \
3 -XX:ShenandoahGCMode=generational ...
到了 Java 25 后, 分代式 Shenandoah GC 是默认的特性了,所以在 Java 25 中只要启用 Shenandoah GC 就是分代式的了
1java -XX:+UseShenandoahGC ...
JEP 479: Remove the Windows 32-bit x86 Port
Deprecate the 32-bit x86 Port for Removal
不再支持 Windows 32 位 x86 平台的, 对其他操作系统 32 位 X86 的也将不再支持
JEP 486: 永久性的禁用 SecurityManager
SecurityManager 是 Java 1.0 就引入的安全机制,允许应用程序定义一个安全策略来限制代码的行为,如访问文件系统,网络,系统属性等。 随着 Java 平台的发展,SecurityManager 已经过时了,Java 17 开始被标记为过时,Java 24 被永久禁用.
在一些老旧的代码中才可能见到 SecurityManager 的使用,新生代的程序也见不到。
JEP 490: ZGC: 移除了非分代的模式
分代收集都是主流 GC 的标本,如 G1 GC, ZGC, 和 Shenandoah GC. 以后只要启动时 java -XX:+UseZGC 就是分代的 ZGC 了。
JEP 491: 虚拟线程在 synchronized 中不再固定平台线程
Java 24 之前,Java 21 引入的虚拟线程碰到 synchronized 的方法的代码块时无法让出所对应的平台线程,也就是说虚拟线程对 synchronized 无效。
Java 24 解决了这一问题,在 synchronized 方法或函数中的 IO 等待也能让出其所在的平台线程。它的解决办法是把原来对 synchronized 监控平台
线程转换为监控虚拟线程。
JEP 495: 简化的源文件和 Main 方法
在 IntelliJ IDEA 中选择了 24 (Preview) - Flexiable constructor bodies, simple source files, etc. 语方级别后,写下面的代码
1public class Demo {
2 public static void main(String[] args) {
3 System.out.println("Hello, World!");
4 }
5}
就会建议写成
1void main() {
2 System.out.println("Hello, World!");
3}
对生成的 Demo.class 反编译后,是下面的样子
1final class Demo {
2 Demo() {
3 }
4
5 void main() {
6 System.out.println("Hello, World!");
7 }
8}
该特性将在 Java 25 中转为正式。即使是
1class Demo {
2 void main() {
3 System.out.println("Hello, World!");
4 }
5}
也才能在 Java 25 中被识别为一个可执行的 main 方法。这种特性其实没什么用,放在哪里也不知道会有多少人会去使用,或不被正式项目接纳,只为了迎合
脚本语言的开发人员。比如上面的代码放在任何一个版本的 Java 中都是合法的,只是在 Java 25 之前不认作为可执行的 main 方法,反而会让代码混乱。
JEP 498: 使用 sun.misc.Unsafe 中内存访问方法将会有警告
sun.misc.Unsafe 与 SecurityManager 同样古老,现在也将被警告
WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
WARNING: sun.misc.Unsafe::setMemory has been called by com.foo.bar.Server (file:/tmp/foobarserver/thing.jar)
WARNING: Please consider reporting this to the maintainers of com.foo.bar.Server
WARNING: sun.misc.Unsafe::setMemory will be removed in a future release
JDK 26 开始将会抛出异常,加上 JVM 参数 --sun-misc-unsafe-memory-access=allow 才能屏蔽掉以上警告信息。
[版权声明]
本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。