JDK19 新特性初体验

1,054 阅读12分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 3 天,点击查看活动详情

 序言

2022 年 9 月 20 日,Java19 发布了!这篇文章通过实例来体验一下 Java19 的新特性

这是自 2018 年 Java10 发布以来,6 个月一个版本的第 10 个发行版。Java SE 产品经理,稳住!

Java 19 不是 LTS,按照 Oracle 的计划,下一个 LTS 将于 2023 年 9 月发布,也就是 Java 21

新特性概览

Java 19 共有 7 个新特性,4 个预览版特性 + 2 个处于孵化阶段特性 + 1 个特性 

JEP编号特性英文名特性中文描述关联项目
405Record Patterns (Preview)Record 模式Amber
422Linux/RISC-V PortLinux/RISC-V 移植-
424Foreign Function & Memory API (Preview) 外部函数和内存 API Panama
425Virtual Threads (Preview) 虚拟线程Loom
426Vector API (Fourth Incubator) 向量APIPanama
427Pattern Matching for switch (Third Preview) switch 模式匹配Amber
428Structured Concurrency (Incubator)结构化并发Loom

新特性详解及代码实战

实战环境信息

操作系统:macOS Monterey 12.6 (M1 Chip)

IDEA 版本:Community 2022.2.3

下载 IDEA 社区版最新版 2022.2.3,下载 JDK19(PS:一台电脑上可以同时安装专业版和社区版)

 在 IDEA 中新建一个工程 jdk-feature,选择已经下载好的 JDK19,Languate Level 选择 19 预览版

在运行时,IDEA 会自动加上 --enable-preview 参数 ,启用预览功能

JEP 405 - Record 模式

产品经理如是说

该 JEP 扩展了模式匹配以表示更复杂,组合更灵活的数据访问方式,极大提高了开发效率。使用记录模式增强 Java 编程语言以便于解构记录值,这可以嵌套记录模式和类型模式,实现强大的、声明式的和可组合的数据导航和处理形式

简单来说就是提供了更多的语法糖,接下来看看这个到底甜不甜

JDK 16 扩展了 instanceof 关键字,引入 Type 模式,在 if 语句块内使用变量时,不用强制类型转换了

public static void testInstanceOf(Object o) {
    // JDK 16以前
    if (o instanceof String) {
	    String s = (String)o;
	    ... 使用 s ...
	}

	// JDK 16+
	if (o instanceof String s) {
	    ... 使用 s ...
	}
}

官网给出的一个 Record 模式示例

public class Jep405Demo {

    record Point(int x, int y) {}

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

    public static void main(String[] args) {
        printSum(new Point(3, 6));
    }
}

这段代码中有一个 record 关键字,表示 Point 是一个 java.lang.Record 类,Records 是JDK 16 发布的新特性,record Point(int x, int y) {} 在编译后,相当于下边的代码(有点 Lombok 的味道了)

class Point {
    // final类型的属性
    private final int x;
    private final int y;

    // 包含全部属性的构造方法
    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    // getter
    int x() { return x; }
    int y() { return y; }

    public boolean equals(Object o) {
        if (!(o instanceof Point)) return false;
        Point other = (Point) o;
        return other.x == x && other.y == y;
    }

    public int hashCode() {
        return Objects.hash(x, y);
    }

    public String toString() {
        return String.format("Point[x=%d, y=%d]", x, y);
    }
}

Record 模式常和 switch 结合使用,接下来介绍 JEP 427,JEP 405 和 JEP 427 都是 Amber 项目下的特性

JEP 427 - switch 模式匹配

 这是 switch 模式匹配的第三次预览,对模式匹配进行扩展,主要是以下 4 点

1.增强类型检查,case 表达式支持多种类型

record Point(int i, int j) {}
enum Color { RED, GREEN, BLUE; }

static void typeTester(Object o) {
    switch (o) {
        case null     -> System.out.println("null");
        case String s -> System.out.println("String");
        case Color c  -> System.out.println("Color: " + c.toString());
        case Point p  -> System.out.println("Record class: " + p.toString());
        case int[] ia -> System.out.println("Array of ints of length" + ia.length);
        default       -> System.out.println("Something else");
    }
}

注意:对于有父子关系的多个子句, 如果父类型在子类型之前,父类型子句会优先匹配,子类型子句将不可达,会抛出编译期错误。应该调换一下顺序

// 正确写法
static void first(Object o) {
    switch (o) {
        case String s ->
            System.out.println("A string: " + s);
        case CharSequence cs ->
            System.out.println("A sequence of length " + cs.length());
        default -> {
            break;
        }
    }
}

// 错误写法,编译错误
static void error(Object o) {
    switch (o) {
        case CharSequence cs ->
            System.out.println("A sequence of length " + cs.length());
        case String s ->    // Error - pattern is dominated by previous pattern
            System.out.println("A string: " + s);
        default -> {
            break;
        }
    }
}

2. switch 表达式和语句分支全覆盖检测

static int coverage(Object o) {
    return switch (o) {         // Error - still not exhaustive
        case String s  -> s.length();
        case Integer i -> i;
    };
}

这段代码的分支没有全覆盖。如果传入 Long 类型,switch 就找不到对应的匹配,因此会报编译错误,IDEA 的提示为:'switch' expression does not cover all possible input values

增加 default 语句后可以消除错误

static int coverage(Object o) {
    return switch (o) {
        case String s  -> s.length();
        case Integer i -> i;
        default -> 0;
    };
}

编译器可以自动检测 JDK 17 发布的特性 sealed 类,判断是否全覆盖。来看一下这段代码

sealed interface S permits A, B, C {}
final class A implements S {}
final class B implements S {}
record C(int i) implements S {}  // Implicitly final

static int testSealedExhaustive(S s) {
    return switch (s) {
        case A a -> 1;
        case B b -> 2;
        case C c -> 3;
    };
}

在这段代码中,sealed 关键字和 permits 声明了接口 S 只能被 A, B, C 三个 final 类实现,因此 3 个 case 表达式已经可以保证全覆盖,就不需要 default 语句了

3.扩展了模式变量声明范围

以下 3 种情况都属于模式变量的可用范围

  • 任意的 when 语句
  • case 语句箭头后的表达式、代码块、throw 语句
  • 一个 case 语句的模式变量范围,不允许越过另一个 case 语句
// 第1条规则
static void test(Object o) {
    switch (o) {
        case Character c
        when c.charValue() == 7:
            System.out.println("Ding!");
            break;
        default: break;
    }
}


// 第2条规则
static void test(Object o) {
    switch (o) {
        case Character c -> {
            if (c.charValue() == 7) {
                System.out.println("Ding!");
            }
            System.out.println("Character");
        }
        case Integer i ->
            throw new IllegalStateException("Invalid Integer argument: "
                                            + i.intValue());
        default -> {}
    }
}

// 第3条规则,编译期错误
// 如果允许这种情况,假设o是Character类型,执行完第1个语句块后,继续执行第2个语句块时,i未初始化
static void test(Object o) {
    switch (o) {
        case Character c:
            if (c.charValue() == 7) {
                System.out.print("Ding ");
            }
            if (c.charValue() == 9) {
                System.out.print("Tab ");
            }
            System.out.println("character");
        case Integer i:
            // Compile-time error
            System.out.println("An integer " + i);
        default: break;
    }
}

4.优化 null 处理,可以声明一个 null case

在没有这个优化之前,一般要这样处理。否则会抛出 NullPointerException

static void test(Object o) {
    if (null == o) {
        return;
    }
    switch (o) {
        case String s  -> System.out.println("String: " + s);
        case Integer i -> System.out.println("Integer");
        default        -> System.out.println("default");
    }
}

现在只需要增加一个 null case 声明

// 显式声明null case
static void test(Object o) {
    switch (o) {
        case null     -> System.out.println("null!");
        case String s -> System.out.println("String");
        default       -> System.out.println("Something else");
    }
}

支持这个语法后,结合 JDK 16 提供的箭头表达式,可以支这样特的特殊表达式

Object o = ...
switch (o) {
    case null, String s -> System.out.println("String, including null");
    ...
}

一个 switch 案例

记得刚开始学 Java 时,有一个题目是用 switch 语法获取一年的各个月份有多少天,写法很繁琐(当时还不知道用 Calendar 提供的方法 calendar.getActualMaximum(Calendar.DAY_OF_MONTH))。但现在就方便多了

public class Jep427Demo {
    static boolean isLeapYear(int year) {
        return (year % 400 == 0) || (year % 4 == 0 && year % 100 != 0);
    }

    static int getDaysOfMonth(int year, int month) {
        return switch(month) {
            case 2 -> isLeapYear(year) ? 29 : 28;
            case 1, 3, 5, 7, 8, 10, 12 -> 31;
            case 4, 6, 9, 11 -> 30;
            default -> 0;
        };
    }

    public static void main(String[] args) {
        System.out.println(getDaysOfMonth(2020, 3));
    }
}

JEP 422 - Linux/RISC-V 移植

通过 Linux/RISC-V 移植,Java 将获得对硬件指令集的支持,该指令集已被广泛的语言工具链支持。RISC-V 是一种包含矢量指令的通用 64 位 ISA,移植后将支持以下的 HotSpot 子系统

  • 模板解释器
  • 客户端 JIT 编译器(C1)
  • 服务端 JIT 编译器(C2)
  • 包含 ZGC 和 Shenandoah 在内的主线垃圾收集器

这个 JEP 的关注点是通过移植集成到 JDK 的主仓库中,而不是移植工作量,该移植基本完成

JEP 424 - 外部函数和内存 API 

引入的一组 API,让 Java 程序与 Java 运行时之外的代码(JVM 之外的代码)和数据(不受 JVM 管理的内存)进行交互,而不通过 JNI 操作 

外部内存

Java 运行时之外的存储数据,常被称为堆外(off-heap)数据,在此之前,对于堆外数据的访问,Java 平台一直没能提供令人满意的方案。ByteBuffer API 安全,但处理效率不高,Unsafe 高效,但不安全

外部函数

JNI 可以调用原生代码,但是这远远不够,JNI 执行过程包含不少架构,开发者需要在多个工具之间来回奔波。此外,JNI 通常是与 C++ 和 C 语言库交互,对其它开发语言支持甚少

基于以上这些痛点,在这个 JEP 中,千呼万唤,FFM API 出现了

这个 JEP 的 4 个目标如下

  1. 易用性:通过卓越的纯 Java 开发模型代替 JNI
  2. 高性能:提供能与当前 JNI 和 sun.misc.Unsafe 相当甚至更优越的性能
  3. 通用性:提供支持不同种类的外部内存(如本地内存、持久化内存和托管堆内存)的 API,并随着时间推移支持其他操作系统甚至其他语言编写的外部函数
  4. 安全性:允许程序对外部内存执行不安全的操作,但默认警告用户此类操作

核心的 API 和功能如下

  • 分配外部内存:MemorySegment、MemoryAddress和SegmentAllocator
  • 操作和访问结构化的外部内存:MemoryLayout和VarHandle
  • 控制外部内存(分配和回收) :MemorySession
  • 调用外部函数:Linker、FunctionDescriptor和SymbolLookup

这些 API 统称为 FFM API,位于 java.base 模块的 java.lang.foreign 包中

由于这一组 API 比较多,且相对复杂,这里用官网给出的一个简单例子来演示

package com.jasonidea.jdk19;

import static java.lang.foreign.ValueLayout.JAVA_INT;
import static java.lang.foreign.MemoryLayout.PathElement;

public class Jep424Demo {

    public static void main(String[] args) {
        new Jep424Demo().memoryOperation();
    }

    public void memoryOperation() {
        /*
         * 1. 创建结构化的顺序内存布局,结构如下
         * struct Point {
         * int x;
         * int y;
         * } pts[10];
         */
        SequenceLayout ptsLayout = MemoryLayout.sequenceLayout(10, MemoryLayout.structLayout(
                JAVA_INT.withName("x"),
                JAVA_INT.withName("y")));

        // 2. 分配内存并对内存设置值
        VarHandle xHandle = ptsLayout.varHandle(PathElement.sequenceElement(), PathElement.groupElement("x"));
        VarHandle yHandle = ptsLayout.varHandle(PathElement.sequenceElement(), PathElement.groupElement("y"));
        MemorySegment segment = MemorySegment.allocateNative(ptsLayout, MemorySession.openImplicit());
        for (int i = 0; i < ptsLayout.elementCount(); i++) {
            xHandle.set(segment,/* index */ (long) i, /* value to write */i); // x
            yHandle.set(segment,/* index */ (long) i, /* value to write */i); // y
            System.out.printf("index => %d, x = %d, y = %d\n", i, i, i);
        }

        // 3. 获取内存值
        int xValue = (int) xHandle.get(segment, 3);
        System.out.println("Point[3].x = " + xValue);
        int yValue = (int) yHandle.get(segment, 6);
        System.out.println("Point[6].y = " + yValue);
    }
}

输出结果如下

index => 0, x = 0, y = 0
index => 1, x = 1, y = 1
index => 2, x = 2, y = 2
index => 3, x = 3, y = 3
index => 4, x = 4, y = 4
index => 5, x = 5, y = 5
index => 6, x = 6, y = 6
index => 7, x = 7, y = 7
index => 8, x = 8, y = 8
index => 9, x = 9, y = 9
Point[3].x = 3
Point[6].y = 6

JEP 426 - 向量 API

向量 API 目前是第四次孵化,功能是表达向量计算,在运行时编译为 CPU 架构上的最佳向量指令,从而实现优于等效标量计算的性能。目前相关 API 都在 jdk.incubator.vector 包下

来看一下官网给出的示例,并打印了一下标量计算和向量计算的执行时间

注意:要在项目中运行处于孵化阶段的特性代码,可以参考这篇文章。简单来说,孵化特性并不在 JDK 核心模块中,而是在 jdk.incubator.xxx 模块下

可以在项目中增加 module-info.java 文件

module ModuleInfo {
    requires jdk.incubator.vector;
    requires jdk.incubator.concurrent;
}

package com.jasonidea.jdk19;

import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorSpecies;

import java.util.Arrays;

public class Jep426Demo {

    static 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;
    private static void vectorComputation(float[] a, float[] b, float[] c) {
        for (int i = 0; i < a.length; i += SPECIES.length()) {
            var m = SPECIES.indexInRange(i, a.length);
            var va = FloatVector.fromArray(SPECIES, a, i, m);
            var vb = FloatVector.fromArray(SPECIES, b, i, m);
            var vc = va.mul(va).add(vb.mul(vb)).neg();
            vc.intoArray(c, i, m);
        }
    }
    public static void main(String[] args) {
        float[] tempA = {1.0f, 3.0f, 2.0f, 4.0f, 8.0f, 10.0f};
        float[] tempB = {1.0f, -1.0f, 5.0f, 3.0f, 8.0f, 9.0f};
        float[] tempC = {1.0f, 6.0f, 1.0f, 1.0f, 1.0f, 1.0f};

        int scaleUpFactor = 10000;
        float[] a = new float[tempA.length * scaleUpFactor];
        float[] b = new float[tempA.length * scaleUpFactor];
        float[] c = new float[tempA.length * scaleUpFactor];
        for (int i = 0; i < scaleUpFactor; i++) {
            for (int j = 0; j < tempA.length; j++) {
                int idx = i * tempA.length + j;
                a[idx] = tempA[j];
                b[idx] = tempB[j];
                c[idx] = tempC[j];
            }
        }

        long startScalar = System.nanoTime();
        scalarComputation(a, b, c);
        System.out.printf("scalar computation cost %d nanoseconds\n", System.nanoTime() - startScalar);
        long startVector = System.nanoTime();
        vectorComputation(a, b, c);
        System.out.printf("vector computation cost %d nanoseconds\n", System.nanoTime() - startVector);
    }
}

运行结果如下

发现向量 API 的执行时间更长,足足差了 40 多倍,Why? 说好的在运行时编译为 CPU 架构上的最佳向量指令呢?

继续查看官网的说明,向量 API 有如下两种实现

  1. 第一种实现是 Java 层面的操作,功能性较强,但没有经过优化
  2. 第二种实现在 C2 编译器层面定义了内部向量操作,便于运行时做优化 

JEP 425 - 虚拟线程

这是处于预览阶段的特性,虚拟线程,也就是轻量级线程。虚拟线程极大地降低了高吞吐量应用的开发和维护成本

平台线程(原有线程)是在 OS 线程上做的封装,它的创建和切换成本很高,可用的线程数量也有限制。对于并发较高的应用,想要提高系统的吞吐量,之前一般是做异步化,但这种方式很难定位线上问题

虚拟线程的引入,让 thread-per-request 风格再次回到开发者的视线,虚拟线程的资源分配和调度由 Java 平台实现,它不再直接与 OS 线程强关联,而是直接将平台线程作为载体线程,这使得虚拟线程的可用数量大大增加

先举一个虚拟线程的简单使用示例

public class Jep425Demo {
	// 创建10000个虚拟线程
	try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
	    IntStream.range(0, 10_000).forEach(i -> {
	        executor.submit(() -> {
	            Thread.sleep(Duration.ofSeconds(1));
	            return i;
	        });
	    });
	}  // try-with-resources,会隐式调用executor.close()

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        firstVirtualThread();
        System.out.printf("firstVirtualThread finished, time cost %d ms\n",
                System.currentTimeMillis() - startTime);
    }
}

这段代码创建了 10000 个虚拟线程并开始执行,各自睡眠 1 秒后结束。现代操作系统可以轻松地支持 10000 个虚拟线程并发执行,虚拟机只需要与很少的 OS 线程交互,或许只有 1 个。执行结果如下,在大约 1 秒后,程序结束

平台线程示例如下

private static void testPlatformThread() {
    try (var executor = Executors.newThreadPerTaskExecutor(Thread.ofPlatform().factory())) {
        IntStream.range(0, 10_000).forEach(i -> {
            executor.submit(() -> {
                Thread.sleep(Duration.ofSeconds(1));
                return i;
            });
        });
    }
}

点击运行,直接资源不足了。根据错误日志,当前已经创建了 4065 个线程,第 4066 个就上限了

注意:这并不意味着虚拟线程比平台线程执行代码的速度更快 ,虚拟线程提供更高的吞吐量,而不是速度更快

但是,虚拟线程在如下两种场景下,才能大幅提高应用系统的吞吐量

  • 并发任务量很大(万级)
  • 线程工作量不会使 CPU 受限(不是 CPU 密集型任务)

虚拟线程创建方式

private static void infoCurrentThread() {
    Thread thread = Thread.currentThread();
    System.out.printf("线程名称: %s,是否虚拟线程: %s\n",
            thread.getName(), thread.isVirtual());
}

private static void waysToCreateVirtualThread() {
    // 方式一:直接启动,虚拟线程名称为""
    Thread.startVirtualThread(() -> infoCurrentThread());

    // 方式二:Builder模式构建
    Thread vt = Thread.ofVirtual().allowSetThreadLocals(false)
            .name("VirtualWorker-", 0)
            .inheritInheritableThreadLocals(false)
            .unstarted(() -> infoCurrentThread());
    vt.start();

    // 方式三:Factory模式构建
    ThreadFactory factory = Thread.ofVirtual().allowSetThreadLocals(false)
            .name("VirtualFactoryWorker-", 0)
            .inheritInheritableThreadLocals(false)
            .factory();
    Thread virtualWorker = factory.newThread(() -> infoCurrentThread());
    virtualWorker.start();

    // 方式四:newVirtualThreadPerTaskExecutor
    try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
        executor.submit(() -> infoCurrentThread());
    }

    // 方式五:构建"虚拟线程池"
    ExecutorService executorService = Executors.newThreadPerTaskExecutor(factory);
    executorService.submit(() -> infoCurrentThread());

    infoCurrentThread();
}

JEP 428 - 结构化并发

结构化并发功能还处于孵化阶段,该功能旨在简化多线程编程。结构化并发提供的特性将在不同线程中运行的多个任务视为一个工作单元,以简化错误处理和取消,提高了可靠性和可观测性

来看一个示例

package com.jasonidea.jdk19;

import jdk.incubator.concurrent.StructuredTaskScope;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

public class Jep428Demo {
    record User(String name, Long id){}
    record Order(String orderNo, Long id){}
    record Response(User user, Order order){}
    private User findUser(){
        return new User("Java", 19L);
    }
    private Order fetchOrder(){
        // return new Order("20221001", 1L);
        throw new UnsupportedOperationException("fetchOrder");
    }
    private Response handle() throws ExecutionException, InterruptedException {
        try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
            Future<User> user = scope.fork(() -> findUser());
            Future<Order> order = scope.fork(() -> fetchOrder());
            scope.join();
            scope.throwIfFailed();  // 如果任意一个子任务失败,抛出异常
            // 到这里时, 两个fork都执行成功了, 结果组合
            return new Response(user.resultNow(), order.resultNow());
        }
    }

    public static void main(String[] args) throws Exception {
        Jep428Demo demo = new Jep428Demo();
        demo.handle();
    }
}

执行这段代码时,fetchOrder 方法会抛出异常,throwIfFailed 检测并重新抛出异常

// 修改fetchOrder方法,不抛出异常
private Order fetchOrder(){
    // throw new UnsupportedOperationException("fetchOrder");
    return new Order("20221001", 1L);  
}

修改 fetchOrder 方法后,再次执行,程序正常退出

小结

以上就是 Java 19 新特性初体验的详细内容。虽然新特性不多,而且大多还处在预览和孵化阶段,但这些特性还是值得期待的(PS:产品经理稳住,快跟不上了~_~)

参考文档 

Oracle - The Arrival of Java 19

JEP 395 - Records

JEP 405 - Record Patterns (Preview)

JEP 422 - Linux/RISC-V Port

JEP 427 - Pattern Matching for switch (Third Preview) 

JEP 424 - Foreign Function & Memory API (Preview) 

JEP 425 - Virtual Threads (Preview)

JEP 428 - Structured Concurrency (Incubator)

pakage jdk.incubator.foreign is not visible error