Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java19 的新特性

你好,我是看山。

本文收录在 《从小工到专家的 Java 进阶之旅》 系列专栏中。

从 2017 年开始,Java 版本更新策略从原来的每两年一个新版本,改为每六个月一个新版本,以快速验证新特性,推动 Java 的发展。让我们跟随 Java 的脚步,配合示例讲解,看一看每个版本的新特性,本期是 Java19 的新特性。

概述

Java19 在 2022 年 9 月 20 日发布GA版本,共七大特性,这些新特性大多处于预览或孵化阶段,旨在简化并发编程、提高性能和增强语言表达能力。这七大特性分别是:

  • JEP 405: Record 模式(预览)
  • JEP 422: Linux/RISC-V移植
  • JEP 424: 外部函数和内存API(预览)
  • JEP 425: 虚拟线程(预览)
  • JEP 426: 向量API(第四次孵化)
  • JEP 427: Switch模式匹配(第三次预览)
  • JEP 428: 结构化并发API (孵化)

接下来我们一起看看这些特性。

JEP 422: Linux/RISC-V 移植

JEP 422 是将 Java 开发环境(JDK)移植到基于 RISC-V 指令集架构的 Linux 系统上。

RISC-V 是一种开源且无版税的指令集架构,最初由加州大学伯克利分校设计,并由 RISC-V 国际基金会共同开发和维护。

该移植项目的目标包括以下几个方面:

  • 支持多种子系统:移植版本将支持模板解释器、C1 和 C2 JIT 编译器以及所有当前的主要垃圾回收器,如 ZGC 和 Shenandoah。
  • 硬件配置支持:目前仅支持 RV64GV 配置,这是一种通用的 64 位 ISA,包含向量指令。未来可能会考虑支持其他 RISC-V 配置,例如 RV32G 的一般用途 32 位配置。
  • 集成到 JDK 主线代码库中:重点在于将移植的内容集成到 JDK 的主仓库中,确保其能够在 RISC-V 架构上正常运行。

此外,RISC-V 架构具有模块化设计,采用精简、可靠且支持多平台的优点,这使得它在嵌入式系统和高性能计算领域有广泛的应用前景。

通过此次移植,Java 将能够更好地适应这些新兴的硬件平台,从而扩展其生态系统并提高其在不同硬件架构上的兼容性和可用性,增强 Java 在嵌入式系统和高性能计算等领域的应用能力.

预览功能

JEP 405: Record 模式(预览)

Record是Java16正式发布的基础类型,提供不可变对象的简单实现(其实就是Java Bean,但是省略一堆的getter、setter、hashcode、equals、toString等方法)(参见Java16 的新特性)。

JEP 405: Record模式归属于Amber项目的一部分,Amber项目旨在通过小而美的方式,增强Java语言特性。本次的Record模式,主要是使Record类型可以直接在instanceof和switch模式匹配中使用。主要体现在4个方面:

  1. 解构记录值:Record模式允许用户对Record的值进行解构,这意味着可以将一个Record类型的实例分解成其各个组成元素。
  2. 嵌套Record模式和instanceof模式:Record模式可以与instanceof模式匹配嵌套使用,从而创建强大、声明性和可组合的数据导航和处理形式。这种嵌套使得数据查询和处理更加灵活和复杂。
  3. 语法和代码生成的变化:为了支持Record模式,Java19需要从语法到代码生成以及DOM模型进行大量的修改。这表明JEP 405不仅在功能上带来了新的特性,还在语言内部结构上进行了深入的调整。
  4. 目标和用途:JEP 405的目标是扩展模式匹配的能力,实现更复杂的数据查询和处理。通过引入Record模式,开发者可以以一种更加声明性和可组合的方式进行数据导航和处理。

我们一起看个示例,比如有下面几个基础元素:

// 颜色
enum Color { RED, GREEN, BLUE}
// 点
record Point(int x, int y) {}
// 带颜色的点
record ColoredPoint(Point p, Color color) {}
// 正方形
record Square(ColoredPoint upperLeft, ColoredPoint lowerRight) {}

我们分别通过instanceof模式匹配和switch模式匹配判断输入参数的类型,打印不同的格式:

private static void instancePatternsAndPrint(Object o) {
    if (o instanceof Square(ColoredPoint upperLeft, ColoredPoint lowerRight)) {
        System.out.println("Square类型:" + upperLeft + " " + lowerRight);
    } else if (o instanceof ColoredPoint(Point(int x, int y), Color color)) {
        System.out.println("ColoredPoint类型:" + x + " " + y + " " + color);
    } else if (o instanceof Point p) {
        System.out.println("Point类型:" + p);
    }
}

private static void switchPatternsAndPrint(Object o) {
    switch (o) {
        case Square(ColoredPoint upperLeft, ColoredPoint lowerRight) -> {
            System.out.println("Square类型:" + upperLeft + " " + lowerRight);
        }
        case ColoredPoint(Point(int x, int y), Color color) -> {
            System.out.println("ColoredPoint类型:" + x + " " + y + " " + color);
        }
        case Point p -> {
            System.out.println("Point类型:" + p);
        }
        default -> throw new IllegalStateException("Unexpected value: " + o);
    }
}

我们通过main方法执行下:

public static void main(String[] args) {
    var p = new Point(1, 2);
    var cp1 = new ColoredPoint(p, Color.RED);
    var cp2 = new ColoredPoint(p, Color.GREEN);
    var square = new Square(cp1, cp2);

    instancePatternsAndPrint(square);
    instancePatternsAndPrint(cp1);
    instancePatternsAndPrint(p);

    switchPatternsAndPrint(square);
    switchPatternsAndPrint(cp1);
    switchPatternsAndPrint(p);
}
// 结果是:
//
// Square类型:ColoredPoint[p=Point[x=1, y=2], color=RED] ColoredPoint[p=Point[x=1, y=2], color=GREEN]
// ColoredPoint类型:1 2 RED
// Point类型:Point[x=1, y=2]
//
// Square类型:ColoredPoint[p=Point[x=1, y=2], color=RED] ColoredPoint[p=Point[x=1, y=2], color=GREEN]
// ColoredPoint类型:1 2 RED
// Point类型:Point[x=1, y=2]

是不是很简洁,就像Java8提供的Lambda表达式一样,很丝滑。

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

JEP 424: 外部函数和内存API(Foreign Function & Memory API,简称FFM API)旨在提供一种机制,使Java程序能够与Java运行时之外的代码和数据进行互操作。通过高效地调用外部函数(即JVM之外的代码)以及安全地访问外部内存(即非JVM管理的内存),该API使得Java程序能够调用本地库并处理本地数据,而无需使用JNI带来的脆弱性和危险。

简单说,FFM API是为了替换JNI的函数,在Java19中是第一次预览版,最终会在Java22中转正。

FFM API的主要功能包括:

  • 易用性:用一个更优越、完全Java开发模型替代了Java原生接口(JNI)。
  • 性能:提供与现有API(如JNI和sun.misc.Unsafe )相当甚至更好的性能。
  • 通用性:支持操作不同类型的外部内存(例如,本地内存、持久内存和托管堆内存),并计划随着时间的推移适应其他平台(例如,32位x86)和用C语言之外的语言(例如C++、Fortran)编写的外部函数。
  • 安全性:允许程序在默认情况下对外部内存执行不安全的操作,但会向用户发出警告。

我们来看一个官方给的示例:如何获取C库函数radixsort的方法句柄,并使用它来对四个字符串进行排序。

// 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

友情提示:官方的代码仅是示意,很多API还在调整,只有等到正式发行时才能暂时确定下来。

JEP 425: 虚拟线程(预览)

虚拟线程是一种轻量级的线程实现,旨在显著降低编写、维护和观察高吞吐量并发应用程序的难度。它们占用的资源少,不需要被池化,可以创建大量虚拟线程,特别适用于IO密集型任务,因为它们可以高效地调度大量虚拟线程来处理并发请求,从而显著提高程序的吞吐量和响应速度。

相比于传统现成,虚拟线程性能显著提升。

虚拟线程有下面几个特点:

  1. 轻量级:虚拟线程是JVM内部实现的轻量级线程,不需要操作系统内核参与,创建和上下文切换的成本远低于传统的操作系统线程(即平台线程),且占用的内存资源较少。
  2. 减少CPU时间消耗:由于虚拟线程不依赖于操作系统平台线程,因此在进行线程切换时耗费的CPU时间会大大减少,从而提高了程序的执行效率。
  3. 简化多线程编程:虚拟线程通过结构化并发API来简化多线程编程,使得开发者可以更容易地编写、维护和观察高吞吐量并发应用程序。
  4. 适用于大量任务场景:虚拟线程非常适合需要创建和销毁大量线程的任务、需要执行大量计算的任务(如数据处理、科学计算等)以及需要实现任务并行执行以提高程序性能的场景。
  5. 提高系统吞吐量:通过对虚拟线程的介绍和与Go协程的对比,可以看出虚拟线程能够大幅提高系统的整体吞吐量。

虽然虚拟线程在Java19中是预览功能,不妨碍我们一起试用下。

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 10_000).forEach(i -> {
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));
            System.out.println(Thread.currentThread().getName() + ": " + i);
            return i;
        });
    });
}

Thread.startVirtualThread(() -> {
    System.out.println("Hello from a virtual thread[Thread.startVirtualThread]");
});

final ThreadFactory factory = Thread.ofVirtual().factory();
factory.newThread(() -> {
            System.out.println("Hello from a virtual thread[ThreadFactory.newThread]");
        })
        .start();

虚拟线程为了降低使用门槛,直接提供了与原生线程类似的方法:

  • Executors.newVirtualThreadPerTaskExecutor(),可以像普通线程池一样创建虚拟线程。
  • Thread.startVirtualThread,通过工具方法直接创建并运行虚拟线程。
  • Thread.ofVirtual().factory().newThread(),另一个工具方法可以创建并运行虚拟线程。Thread还有一个ofPlatform()方法,用来构建普通线程。

通过本地简单测试(在「公众号:看山的小屋」回复”java”获取源码),1w个模拟线程运行时,性能方面虚拟线程 > 线程池。

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

switch模式匹配是老朋友了,最终会在Java21转正。

switch模式匹配允许在switch语句中使用模式来测试表达式,每个模式都有特定的动作,从而可以简洁、安全地表达复杂的数据导向查询。

主要功能包括:

  • 增强表达性和适用性:允许在case标签中出现模式,扩展了switch表达式和语句的表达能力和适用范围。
  • 放宽对null的敌意:当需要时,可以放宽switch对null的敌意。
  • 提高安全性:要求模式switch语句覆盖所有可能的输入值,增加了switch语句的安全性。
  • 保持兼容性:确保所有现有的switch表达式和语句继续无变化地编译并以相同的语义执行。
static String formatValue(Object obj) {
    return switch (obj) {
        case null -> "null";
        case Integer i -> String.format("int %d", i);
        case Long l -> String.format("long %d", l);
        case Double d -> String.format("double %f", d);
        case String s -> String.format("String %s", s);
        default -> obj.toString();
    };
}

public record Person(String name, String address) {}

public static void main(String[] args) {
    System.out.println(formatValue(10));
    System.out.println(formatValue(20L));
    System.out.println(formatValue(3.14));
    System.out.println(formatValue("Hello"));
    System.out.println(formatValue(null));
}

// 运行结果
// int 10
// long 20
// double 3.140000
// String Hello
// null

孵化功能

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

向量API的功能是提供一个表达向量计算的API,这些计算在运行时可靠地编译成支持的CPU架构上的最优向量指令,从而实现比等效标量计算更优的性能。

下面这个是官方给的示例:

// 标量计算示例
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;
    }
}

// 使用向量API的向量计算示例
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;
    }
}

在Java19中,向量API(Vector API)的性能改进主要体现在以下几个方面:

  • 向量计算的编译优化:Java 19中的Vector API能够将向量计算表达式在运行时可靠地编译为支持的CPU架构上的最佳向量指令。这意味着它能够利用特定CPU架构的向量指令集来执行计算,从而实现比等效标量计算更优的性能。
  • 加载和存储向量操作:根据外部函数和内存API预览的定义,在MemorySegment之间加载和存储向量。这使得向量数据的处理更加高效,特别是在需要频繁访问大量数据的情况下。
  • 新增交叉通道向量操作:Java 19还增加了两个新的交叉通道向量操作,即压缩和扩展。这些操作进一步增强了向量计算的能力,使其可以更灵活地处理不同类型的向量数据。
  • 第四轮孵化:Vector API在Java 19中进行了第四轮孵化,这意味着经过前三轮孵化的反馈和改进后,该API已经变得更加成熟和稳定。这种持续的迭代和优化确保了其在实际应用中的可靠性和性能。

向量API在Java中的独特优势在于其高效的并行计算能力、丰富的向量化指令集、跨平台的数据并行算法支持以及对机器学习的特别优化。

JEP 428: 结构化并发API (孵化)

结构化并发API,旨在简化多线程编程。结构化并发是一种多线程编程方法,通过使用新的API来管理多线程代码。将不同线程中的多个任务视为单个工作单元,简化错误处理和提高可靠性。

结构化并发API提供了明确的语法结构来定义子任务的生命周期,并启用一个运行时表示线程间的层次结构。这有助于实现错误传播和取消以及并发程序的有意义观察。

Java使用异常处理机制来管理运行时错误和其他异常。当异常在代码中产生时,如何被传递和处理的过程称为异常传播。

在结构化并发环境中,异常可以通过显式地从当前环境中抛出并传播到更大的环境中去处理。

在Java并发编程中,非受检异常的处理是程序健壮性的重要组成部分。特别是对于非受检异常的处理,这关系到程序在遇到错误时是否能够优雅地继续运行或者至少提供有意义的反馈。

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    var task1 = scope.fork(() -> {
        Thread.sleep(1000);
        return "Result from task 1";
    });

    var task2 = scope.fork(() -> {
        Thread.sleep(2000);
        return "Result from task 2";
    });

    scope.join();
    scope.throwIfFailed(RuntimeException::new);

    System.out.println(task1.get());
    System.out.println(task2.get());
} catch (Exception e) {
    e.printStackTrace();
}

在这个例子中,handle()方法使用StructuredTaskScope来并行执行两个子任务:task1和task2。通过使用try-with-resources语句自动管理资源,并确保所有子任务都在try块结束时正确完成或被取消。这种方式使得线程的生命周期和任务的逻辑结构紧密相关,提高了代码的清晰度和错误处理的效率。使用 StructuredTaskScope 可以确保一些有价值的属性:

  • 错误处理与短路:如果task1或task2子任务中的任何一个失败,另一个如果尚未完成则会被取消。(这由 ShutdownOnFailure 实现的关闭策略来管理;还有其他策略可能)。
  • 取消传播:如果在运行上面方法的线程在调用 join() 之前或之中被中断,则线程在退出作用域时会自动取消两个子任务。
  • 清晰性:设置子任务,等待它们完成或被取消,然后决定是成功(并处理已经完成的子任务的结果)还是失败(子任务已经完成,因此没有更多需要清理的)。
  • 可观察性:线程转储清楚地显示了任务层次结构,其中运行task1或task2的线程被显示为作用域的子任务。

上面的示例能够很好的解决我们的一个痛点,有两个可并行的任务A和B,A+B才是完整结果,任何一个失败,另外一个也不需要成功,结构化并发API就可以很容易的实现这个逻辑。

文末总结

本文介绍了 Java19 新增的特性,完整的特性清单可以从 https://openjdk.org/projects/jdk/19/ 查看。后续内容会发布在 从小工到专家的 Java 进阶之旅 系列专栏中。

青山不改,绿水长流,我们下次见。

推荐阅读


你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。

个人主页:https://www.howardliu.cn
个人博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java19 的新特性
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java19 的新特性

👇🏻欢迎关注我的公众号「看山的小屋」,领取精选资料👇🏻

公众号:看山的小屋