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

你好,我是看山。

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

从 2017 年开始,Java 版本更新策略从原来的每两年一个新版本,改为每六个月一个新版本,以快速验证新特性,推动 Java 的发展。从 《JVM Ecosystem Report 2021》 中可以看出,目前开发环境中有近半的环境使用 Java8,有近半的人转移到了 Java11,随着 Java17 的发布,相信比例会有所变化。

因此,准备出一个系列,配合示例讲解,阐述各个版本的新特性。

概述

Java17 是在 2021 年 9 月发布的一个 LTS 版本(长期支持版本),上一个长期支持版是 Java11,于 2018 年 9 月发布。从目前来看,Java11 在市场占有率已经过半,如果错过了升级 Java11,我们可不要错过这次的升级。接下来我们看看 Java17 为我们带来了哪些新增特性:

  • JEP 306:恢复严格的浮点语义
  • JEP 356:增强型伪随机数生成器
  • JEP 382:新的 macOS 渲染管道
  • JEP 391:macOS/AArch64 端口
  • JEP 398:弃用 Applet API
  • JEP 403:强封装 JDK 内部 API
  • JEP 406:Switch 的模式匹配(预览)
  • JEP 407:删除 RMI 激活机制
  • JEP 409:密封类
  • JEP 410:删除实验性的 AOT 和 JIT 编译器
  • JEP 411:弃用安全管理器
  • JEP 412:外部函数和内存 API(孵化)
  • JEP 414:Vector API(第二版孵化)
  • JEP 415:上下文特定的反序列化过滤器

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

恢复严格的浮点语义(JEP 306)

这个特性是利好科学计算中的浮点运算,保证浮点运算中的 strict 或 strictfp 在每个平台上都能够得到相同的结果,也就是可以把 strictfp 扔了。

在 Java1.2 之前,精确浮点计算是通过迂回的方式实现的。大约从 2001 年开始,奔腾 4 和更高版本的处理器中提供了 SSE2 扩展(数据流单指令多数据扩展指令集 2),可以直接支持严格的 JVM 浮点运算,不需要额外的开销。那个时候 Intel 和 AMD 还不支持这种扩展,于是 Java1.2 的浮点运算就分叉了。

到后来 Intel 和 AMD 也开始支持 SSE2 和更高版本的扩展指令集,Java 语言就可以恢复到严格的浮点运算了。连 Java 之父 James Gosling 在 Twitter 也发文庆祝:

恢复严格的浮点语义(JEP 306)

增强伪随机数生成器(JEP 356)

这个特性是为伪随机数生成器 RPNG(Pseudo-Random Number Generators)增加了新的接口类型和实现,可以更容易地互换使用不同的算法,而且它还为基于流的编程方式提供了更好的支持。这个特性的目标有四个:

  • 在应用程序中更容易地交替使用各种 PRNG 算法;
  • 改进了对基于流的编程的支持,提供了 PRNG 对象流;
  • 消除现有 PRNG 类中的重复代码;
  • 保留java.util.Random类的现有行为,做好向下兼容。

新增了java.util.random.RandomGenerator接口,作为所有 PRNG 算法的统一 API,提供了工厂类java.util.random.RandomGeneratorFactory,借助java.util.ServiceLoader.load()的能力加载各种 PRNG 算法实现,可以构造RandomGenerator实例。

我们遍历一下看看有哪些 PRNG 算法:

RandomGeneratorFactory.all().forEach(factory -> {
    System.out.println(factory.group() + ":" + factory.name());
});

结果是:

LXM:L32X64MixRandom
LXM:L128X128MixRandom
LXM:L64X128MixRandom
Legacy:SecureRandom
LXM:L128X1024MixRandom
LXM:L64X128StarStarRandom
Xoshiro:Xoshiro256PlusPlus
LXM:L64X256MixRandom
Legacy:Random
Xoroshiro:Xoroshiro128PlusPlus
LXM:L128X256MixRandom
Legacy:SplittableRandom
LXM:L64X1024MixRandom

Legacy:Random就是我们常用的java.util.Random,我们来试试看:

RandomGenerator randomGenerator = RandomGeneratorFactory.of("Random")
        .create(System.currentTimeMillis());
System.out.println(randomGenerator.getClass());
System.out.println(randomGenerator.nextInt(10));

结果是:

class java.util.Random
6 (这个值随不同的运行结果不同)

我们还可以使用流式编程方式批量获取随机数:

final IntStream ints = RandomGeneratorFactory.of("L128X128MixRandom")
        .create()
        .ints(10, 0, 100);
System.out.println(Arrays.toString(ints.toArray()));

结果会得到 10 个随机数字数组(每次运行结果不同):

[50, 16, 73, 4, 79, 32, 55, 34, 40, 53]

新的 MacOS 渲染库(JEP 382)

MacOS 为了提升图形渲染性能,在 2018 年 9 月放弃之前的 OpenGL 渲染库,选用了 Apple Metal。从 Java17 开始,Swing API 内部用于渲染 Java 2D 的 API 开始使用新的 Apple Metal 加速渲染 API。

默认情况下,这个功能不启用,需要主动开启:

-Dsun.java2d.metal=true

这个特性改动是属于 API 内部实现,使用上没有任何差别。而且对 MacOS 的系统版本有要求,需要在 MacOS10.14 版本或以上,否则还是会使用 OpenGL 渲染图形。

MacOS/AArch64 端口(JEP 391)

苹果在 2020 年 6 月的 WWDC 的演讲中宣布,将开启长期将 Macintosh 系列从 x64 过渡到 AArch64 的计划,该特性主要是为了适应这种改变。

Linux 的 AArch64 支持是在 Java9 提供的(参见 Java9 的新特性),Windows 的 AArch64 支持是在 Java16 提供的(参见 Java16 的新特性)。

在 Java12 的时候对 AArch64 的支持库进行了统一,只保留了一套维护代码(参见 Java12 的新特性)。

强封装 JDK 内部 API(JEP 403)

在 Java16 中为了改进 JDK 的安全性和可维护性,对内部 API 进行了封装,但是也留了后门,可以使用启动参数--illegal-access控制内部 API 的封装程度。(参见 Java16 的新特性

到了 Java17 中,除了sun.misc.Unsafe可以使用,其他的内部 API 都变成了强封装模式,而且--illegal-access命令也被移除,如果还在命令中添加该参数,会直接报错:

~ $ java -version
openjdk version "17.0.1" 2021-10-19
OpenJDK Runtime Environment (build 17.0.1+12-39)
OpenJDK 64-Bit Server VM (build 17.0.1+12-39, mixed mode, sharing)
~ $ java --illegal-access
Unrecognized option: --illegal-access
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.

密封类(JEP 409)

密封类特性是在 Java15 提供预览版,Java16 提供第二版预览,终于在 Java17 中成为正式功能。该特性限制哪些其他类或接口可以扩展或实现密封组件。

JEP 409 并没有对密封类有新的特性,可以参考 Java15 的新特性Java16 的新特性,这里不再重复。

上下文特定的反序列化过滤器(JEP 415)

Java 对象序列化是一个非常重要的功能,可以透明化远程处理,也促进了 JavaEE 的成功。序列化过程没有问题,但是反序列化过程可能存在危险:

  • 许多情况下传入的数据流内容是通过未知或未经身份验证的客户端获取的;
  • 序列化数据流中可能携带带有攻击者精心构造的恶意代码。

终于在 Java17 中增加了反序列化过滤器,允许应用程序使用 JVM 范围的过滤器工厂,配置特定于上下文和动态选择的反序列化过滤器,该工厂用于为每个反序列化操作选择一个过滤器。

简单点说,就是提前说好可以反序列化哪些类,如果序列化数据流中包含不被允许的类对象,就直接报错。

预览功能

Switch 的模式匹配(JEP 406)

这个特性功能很赞,在 Java14 中正式提供 Switch 表达式特性(参见 Java14 的新特性),本次提供的是 Switch 模式匹配与 instanceof 模式匹配有些类似,是能够在 Switch 表达式实现类型自动转换。

比如:

static String formatterPatternSwitch(Object o) {
    return switch (o) {
        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 -> o.getClass().getSimpleName() + " " + o;
    };
}

public static void main(String[] args) {
    System.out.println(formatterPatternSwitch(null));
    System.out.println(formatterPatternSwitch("1"));
    System.out.println(formatterPatternSwitch(2));
    System.out.println(formatterPatternSwitch(3L));
    System.out.println(formatterPatternSwitch(4.0));
    System.out.println(formatterPatternSwitch(new AtomicLong(5)));
}

结果是:

null
String 1
int 2
long 3
double 4.000000
AtomicLong 5

可以看到,不只是类型自动转换,还可以直接判断是否是null,省了前置判断对象是否是null了。

期待这个功能早日转正。

孵化功能

外部函数和内存 API(JEP 412)

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

通过更加优雅的方式访问外部函数是从 Java14 开始的,经历了多个孵化版本:

  • Java14 的 JEP 370:外部存储器访问 API(孵化)
  • Java15 的 JEP 383:外部存储器访问 API(第二版孵化功能)
  • Java16 的 JEP 389:外部链接器 API(孵化功能)
  • Java16 的 JEP 393:外部存储器访问 API(第三版孵化功能)
  • Java17 的 JEP 412:外部函数和内存 API

可以看出来,虽然一直在孵化,但是功能越来越强大,这一旦孵出来,岂不是超级神兽了。

这一系列的功能都是为了能够在 Java 类中调用 C 语言类库:

private static final SymbolLookup libLookup;

static {
    // loads a particular C library
    var path = JEP412.class.getResource("/print_name.so").getPath();
    System.load(path);
    libLookup = SymbolLookup.loaderLookup();
}

第一步,需要加载我们希望通过 API 调用的目标库。
第二步,我们需要指定目标方法的签名,并最终调用它:

public String getPrintNameFormat(String name) {
    var printMethod = libLookup.lookup("printName");

    if (printMethod.isPresent()) {
        var methodReference = CLinker.getInstance()
            .downcallHandle(
                printMethod.get(),
                MethodType.methodType(MemoryAddress.class, MemoryAddress.class),
                FunctionDescriptor.of(CLinker.C_POINTER, CLinker.C_POINTER)
            );

        try {
            var nativeString = CLinker.toCString(name, newImplicitScope());
            var invokeReturn = methodReference.invoke(nativeString.address());
            var memoryAddress = (MemoryAddress) invokeReturn;
            return CLinker.toJavaString(memoryAddress);
        } catch (Throwable throwable) {
            throw new RuntimeException(throwable);
        }
    }
    throw new RuntimeException("printName function not found.");
}

上面这段代码摘自https://www.baeldung.com/java-17-new-features

Vector API(JEP 414,第二版孵化)

Vector 向量计算 API 是为了处理 SIMD(Single Instruction Multiple Data,单指令多数据)类型的操作,即并行执行的各种指令集。它利用支持向量指令的专用 CPU 硬件,并允许以管道的形式执行此类指令。这种运算方式可以让开发人员实现更高效的代码,充分利用底层硬件的潜力。日常使用包括科学代数线性应用程序、图像处理、字符处理、繁重的算术应用程序,以及任何需要对多个独立操作数应用一个运算的应用程序。

Vector 向量计算 API 是在 Java16 引入(参见 Java16 的新特性),可以在运行时借助 CPU 向量运算指令,实现更优的计算能力。在 Java17 中,针对性能和实现进行了改进,包括字节向量与布尔数组之间进行转换。

原来的向量运算我们需要这样写:

for (var i = 0; i < a.length; i++) {
    c[i] = a[i] * b[i];
}

现在我们可以这样写:

final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED;

for (var 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(vb);
    vc.intoArray(c, i, m);
}

弃用和删除

启用 Applet API(JEP 398)

Applet 是用 Java 编写可以嵌入到网页中的小应用,属于已经过时的技术,很多浏览器已经取消支持。Applet API 在 Java9 的时候标记了过期,在 Java17 标记为删除(@Deprecated(since = "9", forRemoval = true))。

记得我上学的时候,课本上还有这部分内容。

删除 RMI 激活机制(JEP 407)

RMI 激活机制在 Java15 标记了过期(参见 Java15 的新特性),到 Java17 正式删除。这里只是删除了 RMI 激活机制,对于其他 RMI 功能不受影响。

删除实验性的 AOT 和 JIT 编译器(JEP 410)

在 Java9 的 JEP 295 中,引入了实验性的提前编译 jaotc 工具,但是这个特性自从引入依赖用处都不太大,而且需要大量的维护工作,所以在 Java17 中决定删除这个特性。

但是保留了实验性的 Java 级 JVM 编译器接口(JVMCI),这样开发人员也可以继续使用外部构建的编译器版本,并使用 Graal 编译器(GraalVM)进行 JIT 编译。

弃用安全管理器(JEP 411)

Security Manager 在 JDK1.0 时就已经引入,但是它一直都不是保护服务端以及客户端 Java 代码的主要手段,对于如此鸡肋的功能,最终决定标记为删除(@Deprecated(since="17", forRemoval=true))。

文末总结

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

Java17 是 LTS(长期支持版),上个 LTS 版本是 Java11,很多团队已经在生产上切换,相信接下来会有一些团队在测试环境尝鲜。

有人认为 Java8 是神,有人则喜欢不断地尝鲜,你是哪种呢?欢迎在留言说下你在用哪个版本?

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

推荐阅读


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

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

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

公众号:看山的小屋