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

你好,我是看山。

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

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

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

概述

本文讲解一下 Java14 的特性,这个版本带来了不少新特性、功能实用性的增强、GC 的尝试、性能优化等:

  • JEP 305:instanceof 模式匹配 (预览版)
  • JEP 343:打包工具 (孵化)
  • JEP 345:G1 的可识别 NUMA 系统的内存分配
  • JEP 349:JFR 事件流
  • JEP 352:非原子性的字节缓冲区映射
  • JEP 358:NullPointerException 的友好提示信息
  • JEP 359:Record 声明 (预览版)
  • JEP 361:Switch 表达式转正 (第二版预览)
  • JEP 362:弃用 Solaris 和 SPARC 端口
  • JEP 363:删除 CMS 垃圾回收器
  • JEP 364:ZGC 支持 MacOS
  • JEP 365:ZGC 支持 Windows
  • JEP 366:弃用 ParallelScavenge 和 SerialOld GC 的组合
  • JEP 367:移除 Pack200 Tools 和 API
  • JEP 368:文本块 (第二版预览)
  • JEP 370:外部存储器访问 API(孵化)

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

Switch 表达式转正(JEP 361)

Switch 表达式在 Java12 和 Java13 都处于功能预览阶段,到 Java14 终于转正了,从另一个角度,我们可以在生产环境中使用这个功能了。

我们以“判断是否工作日”的例子展示一下,在 Java14 之前:

@Test
void testSwitch() {
    final DayOfWeek day = DayOfWeek.from(LocalDate.now());
    String typeOfDay = "";
    switch (day) {
        case MONDAY:
        case TUESDAY:
        case WEDNESDAY:
        case THURSDAY:
        case FRIDAY:
            typeOfDay = "Working Day";
            break;
        case SATURDAY:
        case SUNDAY:
            typeOfDay = "Rest Day";
            break;
    }

    Assertions.assertFalse(typeOfDay.isEmpty());
}

在 Java14 中:

@Test
void testSwitchExpression() {
    final DayOfWeek day = DayOfWeek.SATURDAY;
    final String typeOfDay = switch (day) {
        case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> {
            System.out.println("Working Day: " + day);
            yield "Working Day";
        }
        case SATURDAY, SUNDAY -> "Day Off";
    };

    Assertions.assertEquals("Day Off", typeOfDay);
}

注意看,例子中我们使用了两种写法,一种是通过yield关键字表示返回结果,一种是在->之后直接返回。

预览功能

和前面的版本一样,Java14 中也提供了一些预览功能,我们可以在预览环境中试用一下。

这次的预览功能包括文本块(第二版预览)、instanceof 模式匹配、Record 声明,接下来我们分别说一下。

文本块(第二版预览,JEP 368)

文本块在 Java13 中首次出现(参见 Java13),本次又提供了两个扩展:

  • \:表示当前语句未换行,与 shell 脚本中的习惯一致;
  • \s:表示单个空格。

我们看个例子:

@Test
void testTextBlock() {
    final String singleLine = "你好,我是看山,公众号「看山的小屋」。不没有换行,而且我的后面多了一个空格 ";
    final String textBlockSingleLine = """
            你好,我是看山,公众号「看山的小屋」。\
            不没有换行,而且我的后面多了一个空格、s""";

    Assertions.assertEquals(singleLine, textBlockSingleLine);
}

个人感觉、是比较实用的,这个功能在 Java15 中转正,值得期待。

instanceof 模式匹配(JEP 305)

instanceof主要用来检查对象类型,作为类型强转前的安全检查。

比如:

@Test
void test() {
    final Object obj1 = "Hello, World!";
    int result = 0;
    if (obj1 instanceof String) {
        String str = (String) obj1;
        result = str.length();
    } else if (obj1 instanceof Number) {
        Number num = (Number) obj1;
        result = num.intValue();
    }

    Assertions.assertEquals(13, result);
}

可以看到,我们每次判断类型之后,需要声明一个判断类型的变量,然后将判断参数强制转换类型,赋值给新声明的变量。这种写法显得繁琐且多余。

于是在 Java14 中对instanceof进行了改进:

@Test
void test1() {
    final Object obj1 = "Hello, World!";
    int result = 0;
    if (obj1 instanceof String str) {
        result = str.length();
    } else if (obj1 instanceof Number num) {
        result = num.intValue();
    }

    Assertions.assertEquals(13, result);
}

不仅如此,instanceof模式匹配的作用域还可以扩展。在if条件判断中,我们都知道&&与判断是会执行所有的表达式,所以使用instanceof模式匹配定义的局部变量继续判断。

比如:

if (obj1 instanceof String str && str.length() > 20) {
    result = str.length();
}

与原来的写法对比,Java14 提供的写法代码更加简洁、可读性更高,能够提出很多冗余繁琐的代码,非常实用的一个特性,这个功能会在 Java16 中转正。

Record 声明(JEP 359)

在 Java14 预览功能中新增了一个关键字record,它是定义不可变数据类型封装类的关键字,主要用在特定领域类上。这个关键字最终会在 Java16 中正式提供。

我们都知道,在 Java 开发中,我们需要定义 POJO 作为数据存储对象,根据规范,POJO 中除了属性是个性化的,其他的比如gettersetterequalshashCodetoString都是模板化的写法,所以为了简便,很多类似 Lombok 的组件提供 Java 类编译时增强,通过在类上定义@Data注解自动添加这些模板化方法。在 Java14 中,我们可以直接使用record解决这个问题。

比如,我们定义一个Person类:

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

我们转换为之前的定义会是一坨下面这种代码:

public final class PersonBefore14 {
    private final String name;
    private final String address;

    public PersonBefore14(String name, String address) {
        this.name = name;
        this.address = address;
    }

    public String name() {
        return name;
    }

    public String address() {
        return address;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        PersonBefore14 that = (PersonBefore14) o;
        return Objects.equals(name, that.name) && Objects.equals(address, that.address);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, address);
    }

    @Override
    public String toString() {
        return "PersonBefore14{" +
                "name='" + name + '\'' +
                ", address='" + address + '\'' +
                '}';
    }
}

我们可以发现 Record 类有如下特征:

  1. 一个构造方法
  2. getter 方法名与属性名相同
  3. equals()hashCode()方法
  4. toString()方法
  5. 类对象和属性被final修饰,所以构造函数是包含所有属性的,而且没有 setter 方法

Class类中也新增了对应的处理方法:

  • getRecordComponents():返回一组java.lang.reflect.RecordComponent对象组成的数组,该数组的元素与Record类中的组件相对应,其顺序与在记录声明中出现的顺序相同,可以从该数组中的每个RecordComponent中提取到组件信息,包括其名称、类型、泛型类型、注释及其访问方法。
  • isRecord():返回所在类是否是 Record 类型,如果是,则返回 true。

看起来,Record 类和 Enum 很像,都是一定的模板类,通过语法糖定义,在 Java 编译过程中,将其编译成特定的格式,功能很好,但如果没有习惯使用,可能会觉得限制太多。

NullPointerException 的友好提示信息(JEP 358)

在没有考虑完全场景的情况下,很容易碰到空指针异常(NullPointerException,简称 NPE)。一般碰到这个异常,根据异常栈信息我们很容易定位到发生异常的代码行,比如:

@Test
void test1() {
    Student s = null;

    Assertions.assertThrows(NullPointerException.class, ()-> s.getName())
            .fillInStackTrace()
            .printStackTrace();
}

如果是在 Java14 之前,这个时候打印出来的异常信息是:

Exception in thread "main" java.lang.NullPointerException
    at cn.howardliu.tutorials.java14.NpeTest.test1(NpeTest.java:20)

对于上面例子,我们可以直接定位到snull,但是下面这个例子呢:

@Test
void test2() {
    Student s = new Student();
    s.setName("看山");

    Assertions.assertThrows(NullPointerException.class, ()-> s.getClazz().getNo())
            .fillInStackTrace()
            .printStackTrace();
}

我们很难判断s或者s中的clazznull,需要查看上下文代码,或者复杂情况还需要添加日志辅助定位问题。

在 Java14 中,对 NullPointerException 异常栈信息做了增强,通过分析程序的字节码信息,能够做到准确地定位到出现 NullPointerException 的变量,并且根据实际源代码打印出详细异常信息。此时,上面例子的异常信息是:

java.lang.NullPointerException: Cannot invoke "cn.howardliu.tutorials.java14.NpeTest$Clazz.getNo()" because the return value of "cn.howardliu.tutorials.java14.NpeTest$Student.getClazz()" is null
    at cn.howardliu.tutorials.java14.NpeTest.test2(NpeTest.java:30)

这样一目了然。

孵化功能

孵化功能是 Java 开发团队让我们提前尝鲜、公测的功能,在 Java9 模块化之后,孵化功能会放在jdk.incubator.中。

外部存储器访问 API(JEP 370)

Java 对象是驻留在堆上,但是有时候因为其算法或者内存结构的原因,使用效率低下、性能低下、受垃圾收集器 GC 算法影响。所以很多时候我们会使用本机内存或者称为直接内存。

在 Java14 之前,使用直接内存我们会用到ByteBuffer或者Unsafe,但是这两个都存在一些问题。

  • ByteBuffer管理内存最大不能够超过 2G;
  • ByteBuffer管理的这部分内存需要使用垃圾收集器回收内存,使用不当可能造成内存泄漏;
  • Unsafe是非标准的 Java API,可能会因为不合法的内存使用致使系统崩溃。

“天下苦秦久矣”,于是在 Java14 中提供了新的 API:

  • MemorySegment:用来申请内存区域,可以是堆内存,也可以是对外内存;
  • MemoryAddress:从MemorySegment实例获取已申请内存的内存地址用于执行操作,例如从底层内存段的内存中检索数据;
  • MemoryLayout:用来描述内存段的内容,它允许我们定义如何将内存分解为元素,并提供每个元素的大小。

这部分功能截止到 Java17 还是孵化功能,而且内容比较多,后续会单独开一篇介绍。

打包工具(JEP 343)

一般来说,Java 程序会以一个 Jar 的形式提供,web 服务可能是 war 或者 ear 包,但是有时候我们的 Java 程序可能是在自己的 PC 机(比如 Windows 或者 MacOS)上运行,期望可以通过双击打开的方式。

于是 Java14 引入了引入了 jdk.incubator.jpackage.jmod,基于 JavaFX javapackager tool,其目的就是创建一个打包工具,可以将 jar 包构建成 exe、pkg、dmg、deb、rpm 格式的安装文件。

我们可以使用jlink创建一个最小可运行的模块包,然后使用jpackage将其构建成安装包:

jpackage --name myapp --input lib --main-jar main.jar

JVM 功能

ZGC 支持 MacOS 和 Windows 系统(JEP 364、JEP 365)

ZGC 最初是在 Java11 中引入(参见 Java11 的新特性),在后续版本中,不断升级优化,实现可伸缩、低延迟的目标,使用了内存读屏障、染色指针、内存多重映射等技术。在之前,ZGC 只支持 Linux/x64 平台,在 Java14 之后,支持了 macOS/x64 和 Windows/x64 系统中。

开启参数:

-XX:+UnlockExperimentalVMOptions -XX:+UseZGC

G1 的可识别 NUMA 系统的内存分配(JEP 345)

Java14 改进了非一致性内存访问(Non-uniform Memory Access,NUMA)系统上的 G1 垃圾收集器的整体性能,主要是对年轻代的内存分配做出优化,从而提升 CPU 计算过程中内存访问速度。

NUMA 主要是指在多插槽物理计算机体系中,处理器一般是多核,且越来越多具备 NUMA 访问体系结构,即内存与每个插槽或内核之间的距离不等。套接字之间的内存访问有不同的性能特征,更远的套接字访问会有更多的时间消耗。这样的结果是,每个核对于某一区域的内存访问速度会随核与物理内存的位置远近有所差异。

Java 分配内存时,G1 会申请一块 region,作为对象存放区域。如果能够感知 NUMA,就可以优先在当前线程绑定的 NUMA 节点空闲内存执行申请内存操作,用于提升访问速度。

启用参数是-XX:+UseNUMA

JFR 事件流(JEP 349)

飞行记录器(Flight Recorder)是在 Java11 中引入(参见 Java11 的新特性)。

本次增强可以实现 JFR 数据的公开访问,可以通过使用jdk.jfr.consumer中的方法持续读取或流式传输读取记录,用于持续监控。这样的话,我们可以与现有监控系统集成,实现 JFR 数据的持续监听,不用非得等着收集完成后再解析分析。

其他

非原子性的字节缓冲区映射(JEP 352)

FileChannel进行扩展,定义了jdk.nio.mapmode.ExtendedMapMode,用来创建MappedByteBuffer实例,可以对非原子性的字节缓冲区映射(Non-Volatile Mapped Byte Buffers,NVM)实现持久化。

删除 CMS 垃圾回收器(JEP 363)

CMS 是老年代垃圾回收算法,通过标记-清除的方式进行内存回收,在内存回收过程中能够与用户线程并行执行。在 G1 之前,CMS 几乎占据了 GC 的全部江山。在使用过程中,一般是 CMS 与 Parallel New 搭配使用。

CMS 由于其算法特性,会产生内存碎片和浮动垃圾,随着时间推移,可能出现的情况是,虽然老年代还有空间,但是没有办法分配足够内存给大对象。

所以在 Java9 中开始放弃使用 CMS,在 Java14 中彻底删除,并删除与 CMS 有关的参数。从 Java14 开始,CMS 成为了历史。

弃用 ParallelScavenge 和 SerialOld GC 的组合(JEP 366)

Parallel Scavenge 是并行收集算法,SerialOld 提供老年代串行收集,这种年轻代使用并行算法、老年代使用串行算法的混搭的方式,使用场景少且有风险。但是却需要大量工作量维护,所以在 Java14 中,删除了这两种 GC 组合。

删除组合的方式是通过启用组合参数-XX:+UseParallelGC -XX:-UseParallelOldGC,并在单独使用-XX:-UseParallelOldGC时会收到警告信息。

移除 Pack200 Tools 和 API(JEP 367)

these were deprecated for removal in Java 11, and now removed

删除java.util.jar包中的pack200unpack200工具以及 Pack200 API。这些工具和 API 已在 Java11 时标记弃用,删除也是意料之中。

弃用 Solaris 和 SPARC 端口(JEP 362)

Solaris 和 SPARC 都已被 Linux 操作系统和英特尔处理器取代。放弃对 Solaris 和 SPARC 端口的支持将使 OpenJDK 社区的贡献者能够加速开发新功能,从而推动平台向前发展。

这些 API 在 Java14 中标记弃用,在 Java15 中彻底删除。这样做,也是为了让很多正在进行的项目尽早适应新的架构。

文末总结

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

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

推荐阅读


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

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

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

公众号:看山的小屋