侧边栏壁纸
博主头像
ZHD的小窝博主等级

行动起来,活在当下

  • 累计撰写 79 篇文章
  • 累计创建 53 个标签
  • 累计收到 1 条评论

目 录CONTENT

文章目录

JDK22的新特性

江南的风
2024-07-17 / 0 评论 / 0 点赞 / 38 阅读 / 14251 字 / 正在检测是否收录...

了解不同版本的特性有助于我们在项目中选择最合适自己的版本来进行技术选型和开发

Java 22 新特性

1. G1的区域锚定

参考链接:https://openjdk.org/jeps/423

属于GC优化范畴,大概意思是为了降低在使用JNI(Java Native Interface)时G1垃圾收集器(Garbage Collector, GC)引起的延时问题,主要策略是引入了Region Pinning机制。JNI与Java代码的一个显著差异在于,JNI交互可能会直接通过显式指针访问堆中的对象。当Java线程执行包含JNI调用的关键代码段时,这些对象在堆中的位置若被移动,将会导致指针失效,因为JNI代码依赖于这些固定位置的指针。

传统上,G1 GC在识别到线程正在执行此类关键JNI代码段时,会暂时禁用垃圾收集,以防止对象被移动。然而,这种做法可能引发新的问题:如果其他非执行JNI代码的线程触发了GC请求,这些请求将被挂起等待,从而导致系统整体的延迟增加。

为了克服这一挑战,G1 GC现在通过Region Pinning技术进行了优化。这项技术允许G1 GC在JNI代码执行的关键期间,固定那些被JNI代码直接访问的内存区域,即“pin”住这些区域,防止它们在GC过程中被移动或回收。与此同时,G1 GC仍然可以自由地重新定位和收集其他非固定区域的内存,即便有线程正处于JNI代码的关键执行阶段。这种并行处理能力显著降低了因JNI交互而引入的GC延迟,提高了系统的整体性能和响应速度。

2. 子类调用父类构造函数super(...)可以放在逻辑代码后面(预览版)

参考链接:https://openjdk.org/jeps/447

Java是一种面向对象的语言,它允许你扩展非最终的类来继承其状态和行为。但在继承时,为了确保类的正确初始化,Java要求必须先调用超类的构造函数,而且这一调用必须是子类构造函数中的第一条语句。这意味着,在调用超类构造函数之前,你不能对子类的构造函数参数进行任何处理或测试。如果测试发现参数不合法并需要抛出异常,那么已经进行的超类构造函数调用就显得不必要且可能带来问题。

以前版本:

public class Subclass extends SuperClass {

    public Subclass(long value) {
        super(value);               // Potentially unnecessary work
        if (value <= 0)
            throw new IllegalArgumentException(non-positive value);
    }
}

22以后的版本:

public class Subclass extends SuperClass {

    public Subclass(long value) {
        if (value <= 0)
            throw new IllegalArgumentException(non-positive value);
        super(value);
    }
}

3. 外部函数和内存API

参考链接:https://openjdk.org/jeps/454

通过该API,Java程序可以与Java运行时环境之外的代码和数据进行互操作。通过高效地调用外部函数(即JVM之外的代码)和安全地访问外部内存(即不由JVM管理的内存),该API使Java程序能够调用本地库并处理本地数据,同时避免了Java本地接口(JNI)的脆弱性和潜在危险。

例如:

// 1. 在C库路径上查找外部函数
Linker linker = Linker.nativeLinker(); // 获取本地链接器
SymbolLookup stdlib = linker.defaultLookup(); // 获取默认的符号查找器
MethodHandle radixsort = linker.downcallHandle(stdlib.find("radixsort"), ...); // 查找并获取“radixsort”函数的MethodHandle
// 2. 在堆上分配内存以存储四个字符串
String[] javaStrings = { "mouse", "cat", "dog", "car" }; // Java字符串数组
// 3. 使用try-with-resources管理非堆内存的生命周期
try (Arena offHeap = Arena.ofConfined()) { // 创建一个受限的Arena用于管理非堆内存
    // 4. 在非堆内存中分配一个区域以存储四个指针
    MemorySegment pointers = offHeap.allocate(ValueLayout.ADDRESS, javaStrings.length); // 分配内存以存储地址
    // 5. 将字符串从堆复制到非堆 
    for (int i = 0; i < javaStrings.length; i++) {  
        MemorySegment cString = offHeap.allocateFrom(javaStrings[i]); // 为每个Java字符串分配非堆内存  
        pointers.setAtIndex(ValueLayout.ADDRESS, i, cString); // 将C字符串的地址存储在指针数组中  
    }
    // 6. 调用外部函数对非堆数据进行排序  
    radixsort.invoke(pointers, javaStrings.length, MemorySegment.NULL, '\0'); // 调用radixsort函数进行排序  
    // 7. 将(重新排序后的)字符串从非堆复制到堆  
    for (int i = 0; i < javaStrings.length; i++) {  
        MemorySegment cString = pointers.getAtIndex(ValueLayout.ADDRESS, i); // 获取C字符串的地址  
        javaStrings[i] = cString.reinterpret(...).getString(0); // 将C字符串转换回Java字符串并更新数组  
    }
}
// 8. 这里自动释放所有非堆内存
assert Arrays.equals(javaStrings, new String[] {"car", "cat", "dog", "mouse"}); // 验证排序结果,应为true

3. 未命名变量和模式

参考链接:https://openjdk.org/jeps/456

支持用_来替代没有使用的变量声明

例如:

static boolean isDelayTimeEqualToReverbRoomSize(EffectLoop effectLoop) {
    if (effectLoop instanceof EffectLoop(Delay(int timeInMs), Reverb(_, int roomSize))) {
        return timeInMs == roomSize;
    }
    return false;
}

var lesPaul = new Guitar("Les Paul");
try { 
    cart.add(stock.get(lesPaul, guitarCount));
} catch (OutOfStockException _) { 
    System.out.println("Sorry, out of stock!");
}

4. 类文件 API(预览版)

参考链接:https://openjdk.org/jeps/457

提供了一个标准的API,旨在简化Java类文件的解析、生成与转换过程。目前该API处于预览阶段。Java领域内,像ASM、BCEL、Javassist等库广泛用于处理Java类文件,这些库也是大多数字节码生成框架的基础。由于Java类文件格式随着每6个月发布的新版本不断演进,字节码生成框架必须同步更新,以确保对新语言特性的支持。

JDK内部就采用了ASM等工具来实现部分功能和lambda表达式的底层支持。然而,这带来了一个问题:当Java版本更新时,新功能的支持往往滞后于ASM等库的更新,因为开发者需要等待适配新版本的库发布后才能使用。

为了解决这一问题,类文件API被引入JDK中,它直接提供了一个内置的、用于解析、生成和转换Java类文件的接口,从而减少了依赖外部库的延迟,确保了Java类文件处理与Java语言演进的同步。

5. 启动多文件源代码程序

参考链接:https://openjdk.org/jeps/458

JDK 11通过引入JEP 330,增加了直接执行单个Java源文件的能力,但这一功能限制在于所有代码必须集中在一个文件中。面对多文件项目,用户仍需通过编译步骤来整合代码,这通常意味着需要使用如Maven或Gradle等构建工具。对于初学者来说,这可能会成为学习路上的一个障碍,因为他们可能不得不暂时放下Java语言的学习,转而学习如何使用这些构建工具。

而在Java22,引入了一个新的启动器功能,它允许直接运行包含多个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/*来指定,如:


libs/
├─ library1.jar
├─ library2.jar

java --class-path 'lib/*' Prog.java

如果libs里头的是模块化的,则可以使用如下方式启动:

java -p lib Prog.java

6. 字符串模板(第二轮预览)

参考链接:https://openjdk.org/jeps/459

Java 21的JEP430引入了String Templates,Java 22作为第二次preview

例如:

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>
        """;

7. 向量 API(第七轮孵化)

参考链接:https://openjdk.org/jeps/460

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;
    }
}

8. 流搜集器(预览)

参考链接:https://openjdk.org/jeps/461

JDK22针对stream引入了gather操作,允许用户自定义中间操作,比如自定义distinctBy操作

guitars.stream()
        .gather(distinctBy(Guitar::guitarStyle))
        .forEach(System.out::println);

static <T, A> Gatherer<T, ?, T> distinctBy(Function<? super T, ? extends A> classifier) {
    Supplier<Map<A, List<T>>> initializer = HashMap::new;
    Gatherer.Integrator<Map<A, List<T>>, T, T> integrator = (state, element, _) -> {
        state.computeIfAbsent(classifier.apply(element), _ -> new ArrayList<>()).add(element);
        return true; // true, because more elements need to be consumed
    };
    BiConsumer<Map<A, List<T>>, Gatherer.Downstream<? super T>> finisher = (state, downstream) -> {
        state.forEach((_, value) -> downstream.push(value.getLast()));
    };
    return Gatherer.ofSequential(initializer, integrator, finisher);
}        

9. 结构化并发(第二预览版)

参考链接:https://openjdk.org/jeps/462

通过引入结构化并发的API来简化并发编程。结构化并发将运行在不同线程中的相关任务组视为一个单一的工作单元,从而简化了错误处理和取消操作,提高了可靠性,并增强了可观察性。这是一个预览版的API。

例如:

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

        // 在这里,两个子任务都已成功,因此请组合它们的结果
        return new Response(user.get(), order.get());
    }
}

10. 隐式声明类和实例主方法(第二预览版

参考链接:https://openjdk.org/jeps/463

JDK21的JEP445作为首次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");
}

*其中main方法选择的优先顺序是static的优于非static的,然后有args的优于没有args的

11. 作用域值(第二预览版

参考链接:https://openjdk.org/jeps/464

引入作用域值(Scoped Values),它们允许在同一线程的子框架之间以及与子线程之间安全地共享不可变数据。与线程局部变量(thread-local variables)相比,作用域值更容易理解,并且在空间和时间成本上更低,特别是在与虚拟线程(Virtual Threads)和结构化并发(Structured Concurrency)结合使用时。这是一个预览版的API。

例如:

class Server {
  public final static ScopedValue<User> LOGGED_IN_USER = ScopedValue.newInstance();
 
  private void serve(Request request) {
    // ...
    User loggedInUser = authenticateUser(request);
    ScopedValue.where(LOGGED_IN_USER, loggedInUser)
               .run(() -> restAdapter.processRequest(request));
    // ...
  }
}

通过ScopedValue.where可以绑定ScopedValue的值,然后在run方法里可以使用,方法执行完毕自行释放,可以被垃圾收集器回收

0

评论区