Bootstrap

Java 18&Java 19新特性概述

一、Java 18

Java 18于2022年3月22日发布,共有9个特性,本次精选5个特性:

更多内容请读者自行阅读:OpenJDK Java 18官方文档

1.1 JEP 400:Java字符集默认为UTF-8

在Java 18之前,用于读和写的API,比如FileReaader、FileWriter,都有charset参数用于指定字符集,如果没传charset的话,字符集则由JDK启动时的运行环境选择默认的字符集,这就会导致一个问题:在没有指定字符集的情况下,在Mac上运行正常的一段打印文字的Java程序,转到windows上就会出现乱码。

所以在Java 18,JDK将UTF-8设置为默认字符集,在没有指定charset的情况下,不再由JDK运行环境确定。

1.2 JEP 416:反射优化

Java 18改进了反射功能,在 java.lang.invoke 方法句柄之上重新实现 java.lang.reflect.Method、Constructor 和 Field,使之性能更好。

下面是OpenJDK官方给出的新老实现反射性能基准测试结果:

1.3 JEP 417:向量API(第三轮孵化)

  • Vector API 最初由 JEP 338 提出,并作为孵化 API 集成到 Java 16 中。

  • JEP 414 提出了第二轮孵化,并集成到 Java 17 中。

  • 该JEP建议在Java 18中进行第三轮孵化。

Java 18也对向量API进行了微调,有两点改动:

  • 支持ARM标量向量扩展(SVE)平台。

  • 提高在支持硬件掩码的架构上接受掩码的矢量操作的性能。

这个在Java 16时介绍过,这里就不再赘述了。

1.4 JEP 419:外部函数和内存API(第二轮孵化)

  • 外部函数和内存API是由JEP 412提出的,并在2021年中期作为孵化API于Java 17。它结合了两个早期的孵化 API:Foreign-Memory Access API 和 Foreign Linker API。

  • 此 JEP 提议根据反馈进行改进,并在 Java 18 中重新孵化 API。

ps:第一个预览版本在Java 19提出,所以将会在Java 19再详细介绍。

1.5 JEP 420:Switch模式匹配(二次预览)

  • switch 的模式匹配由 JEP 406 提出,并在 JDK 17 中作为预览功能提供。

  • 此 JEP 建议在 JDK 18 中对该功能进行第二次预览,并根据经验和反馈进行细微的改进。

二、Java 19

Java 19发布于2022年9月29日,共有7个特性,本次精选6个:

更多内容请读者自行阅读:OpenJDK Java 19官方文档

2.1 JEP 405:Record 模式(一次预览)

我们回忆下,在 JDK 16 中,JEP 394 扩展了 instanceof 运算符(不必强转直接用):

if(obj instanceof String str){
   System.out.println("字符串:"+str);
}

然后在Java 17、Java 18还扩展了和Switch的结合使用。

还记得Java 14引入的Records类吗,在Java 19用instanceof进一步增强了Records类的语法:

public record Point(int x, int y) {}
​
void printSum(Object o){
    if(o instanceof Point(int x,int y)){
        System.out.println(x+y);
    }
}

2.2 JEP 424:外部函数和内存API(一次预览)

外部函数和内存(FFM)API结合了两个早期的孵化API:外部内存访问API(JEPs 370383393)和外部链接器API(JEP 389)。

  • FFM API 通过 JEP 412 在 JDK 17 中孵化。

  • 并通过 JEP 419 在 JDK 18 中重新孵化。

  • 在JDK 19中,外部函数和内存API不再孵化;相反,它是一个预览 API

通过该 API,Java 程序可以与 Java 运行时之外的代码和数据进行互操作。通过高效调用外部函数(即 JVM 外部的代码)和安全访问外部内存(即不受 JVM 管理的内存),API 使 Java 程序能够调用本机库并处理本机数据,而不会出现 JNI 的脆弱性和危险性。

前面也提到过,在没有外部函数和内存API之前,开发人员如果想要操作堆外内存,通常的做法就是使用ByteBuffer、Unsafe或JNI等方式,但无论哪种方式都无法有效解决安全性和高效性等2个问题,并且堆外内存的释放也是一个头疼的问题。

为了解决这些痛点,The Foreign Function & Memory API (FFM API)定义了类和接口:

  • 分配堆外内存:MemorySegmentMemoryAddressSegmentAllocator。

  • 操作和访问结构化的堆外内存:MemoryLayout,VarHandle。

  • 控制堆外内存的分配和释放:MemorySession。

  • 调用外部函数:Linker,FunctionDescriptor, andSymbolLookup。

下面是FFM API的使用示例(来自官方文档),这段代码获取了C库函数的radixsort方法句柄,然后使用它对Java数组中的四个字符串进行排序:

public static void main(String[] args) {
    // 1. 在C库路径上查找外部函数
    Linker linker = Linker.nativeLinker();
    SymbolLookup stdlib = linker.defaultLookup();
    MethodHandle radixSort = linker.downcallHandle(
            stdlib.lookup("radixsort"), ...);
    // 2. 分配堆上内存以存储四个字符串
    String[] javaStrings   = { "mouse", "cat", "dog", "car" };
    // 3. 分配堆外内存以存储四个指针
    SegmentAllocator allocator = SegmentAllocator.implicitAllocator();
    MemorySegment offHeap  = allocator.allocateArray(ValueLayout.ADDRESS, javaStrings.length);
    // 4. 将字符串从堆上复制到堆外
    for (int i = 0; i < javaStrings.length; i++) {
        // 在堆外分配一个字符串,然后存储指向它的指针
        MemorySegment cString = allocator.allocateUtf8String(javaStrings[i]);
        offHeap.setAtIndex(ValueLayout.ADDRESS, i, cString);
    }
    // 5.  通过调用外部函数对堆外数据进行排序
    radixSort.invoke(offHeap, javaStrings.length, MemoryAddress.NULL, '\0');
    // 6. 将(重新排序的)字符串从堆外复制到堆上
    for (int i = 0; i < javaStrings.length; i++) {
        MemoryAddress cStringPtr = offHeap.getAtIndex(ValueLayout.ADDRESS, i);
        javaStrings[i] = cStringPtr.getUtf8String(0);
    }
    assert Arrays.equals(javaStrings, new String[] {"car", "cat", "dog", "mouse"});  // true
}

2.3 JEP 425:虚拟线程(一次预览)

这个参考:Java 21的虚拟线程是怎么回事

2.4 JEP 426:向量API(第四轮孵化)

  • Vector API 最初由 JEP 338 提出,并作为孵化 API 集成到 JDK 16 中。

  • JEP 414 提出了第二轮孵化,并集成到 JDK 17 中。

  • JEP 417 提出了第三轮孵化,并集成到 JDK 18 中。

  • 此JEP建议在Java 19继续第四轮孵化。

这个在Java 16时介绍过,这里就不再赘述了。

2.5 JEP 427:Switch的模式匹配(第三次预览)

  • JEP 406 提议将 switch 的模式匹配作为预览功能,并在 JDK 17 中提供。

  • JEP 420 提议作为第二个预览,并在 JDK 18 中提供。

  • 此 JEP 提出了第三个预览版,并根据持续的经验和反馈进行了进一步改进。

在Java 19允许case null的选项,这意味着不用在switch之前判null了:

void caseNull(Object o){
    //不用再提前校验
//        if(o == null){
//            System.out.println("o is null");
//        }
    switch (o){
        case null -> System.out.println("o is null");
        case String s-> System.out.println("String:"+s);
        default -> System.out.println("Something else");
    }
}

2.6 JEP 428:结构化并发(第一轮孵化)

Java 19引入了结构化并发,一种多线程编程方式,目的是为了通过结构化并发API来简化多线程编程,目前处于孵化阶段。

结构化并发 API 的主要类是 StructuredTaskScope。这个类允许开发者将一个任务组织成一组并发子任务,并将它们作为一个整体进行协调。子任务在各自的线程中执行,通过分别分叉(fork)它们,然后作为一个整体进行合并(join),并且可能作为一个整体进行取消。子任务的成功结果或异常由父任务进行聚合和处理。StructuredTaskScope 将子任务或分叉的生命周期限制在一个明确的词法范围内,在这个范围内,任务与其子任务的所有交互——包括分叉、合并、取消、处理错误和结果组合——都发生在此范围内。

StructuredTaskScope一般工作流程如下:

  • 创建一个作用域(scope)。创建作用域的线程是它的所有者。

  • 在作用域中分叉(fork)并发子任务。

  • 作用域中的任何分叉或作用域的所有者都可以调用作用域的 shutdown() 方法来请求取消所有剩余的子任务。

  • 作用域的所有者作为一个整体加入作用域,即所有分叉。所有者可以调用作用域的 join() 方法,该方法会阻塞直到所有分叉要么完成(成功或失败),要么通过 shutdown() 被取消。或者,所有者可以调用作用域的 joinUntil(java.time.Instant) 方法,该方法接受一个截止时间。

  • 加入后,处理任何分叉中的错误并处理它们的结果。

  • 关闭作用域,通常通过 try-with-resources 隐式完成。这会关闭作用域并等待任何滞后的分叉完成。

这是官方给的一个示例:

Response handle() throws ExecutionException, InterruptedException {
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        //使用fork方法派生线程来执行任务
        Future<String> user  = scope.fork(() -> findUser());
        Future<Integer> order = scope.fork(() -> fetchOrder());
​
        scope.join();           // 将两个fork合并
        scope.throwIfFailed();  // ...出现异常(抛异常)
​
        // 代表这两个fork都成功了,组合它们的结果。
        return new Response(user.resultNow(), order.resultNow());
    }
}

END:更多新特性的介绍,推荐移步至👉 Java新特性学习导航(8~23 持续更新)👈  

;