JDK19新特性初体验

文章目录

序言

新特性概览

新特性详解及代码实战

JEP 405 - Record模式

JEP 427 - switch模式匹配

JEP 422 - Linux/RISC-V移植

JEP 424 - 外部函数和内存API 

JEP 426 - 向量API

JEP 425 - 虚拟线程

JEP 428 - 结构化并发

小结

参考文档


序言

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

猜你喜欢

转载自blog.csdn.net/u013481793/article/details/127175885