Java12 的新特性

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

你好,我是看山。

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

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

概述

本文讲解一下 Java12 的特性,作为第一个长期支持版 Java11 之后的第一个版本,增加的功能也不少,除了一些小幅度的 API 增强,增加了另一个试验阶段的垃圾收集器 Shenandoah、对 G1 做了优化、增加微基准套件等。

语法特性

Java12 提供了很多的语法特性,既有小而美的增强 API,又有特别方便的工具扩展。本节我们跟着代码看看比较好玩的功能。

String 的增强方法:indent 和 transform

在 Java12 中,String 又增强了两个方法。之所以说又,是因为在 Java11 中已经增加过小而美的方法,想要详细了解的可以查看 Java11 新特性

这次增加的方法是indent(缩进)和transform(转换)。

顾名思义,indent方法是对字符串每行(使用\r\n分隔)数据缩进指定空白字符,参数是 int 类型。

如果参数大于 0,就缩进指定数量的空格;如果参数小于 0,就将左侧的空字符删除指定数量,即右移。

我们看下源码:

public String indent(int n) {
    if (isEmpty()) {
        return "";
    }
    Stream<String> stream = lines();
    if (n > 0) {
        final String spaces = " ".repeat(n);
        stream = stream.map(s -> spaces + s);
    } else if (n == Integer.MIN_VALUE) {
        stream = stream.map(s -> s.stripLeading());
    } else if (n < 0) {
        stream = stream.map(s -> s.substring(Math.min(-n, s.indexOfNonWhitespace())));
    }
    return stream.collect(Collectors.joining("\n", "", "\n"));
}

这里会使用到 Java11 增加的linesrepeatstripLeading等方法。indent最后会将多行数据通过Collectors.joining("\n", "", "\n")方法拼接,结果会有两点需要注意:

  • \r会被替换成\n
  • 如果原字符串是多行数据,最后一行的结尾没有\n,最后会补上一个\n,即多了一个空行。

我们看下测试代码:

@Test
void testIndent() {
    final String text = "\t\t\t 你好,我是看山。\n \u0020\u2005Java12 的 新特性。\r 欢迎三连+关注哟";
    assertEquals("    \t\t\t 你好,我是看山。\n     \u0020\u2005Java12 的 新特性。\n    欢迎三连+关注哟、n", text.indent(4));
    assertEquals("\t 你好,我是看山。\n\u2005Java12 的 新特性。\n 欢迎三连+关注哟、n", text.indent(-2));

    final String text2 = "山水有相逢";
    assertEquals("山水有相逢", text2);
}

我们再来看看transform方法,源码一目了然:

public <R> R transform(Function<? super String, ? extends R> f) {
    return f.apply(this);
}

通过传入的Function对当前字符串进行转换,转换结果由Function决定。比如,我们要对字符串反转:

@Test
void testTransform() {
    final String text = "看山是山";
    final String reverseText = text.transform(s -> new StringBuilder(s).reverse().toString());
    assertEquals("山是山看", reverseText);
}

其实这个方法在 Java8 中提供的Optional实现类似的功能(完整的 Optional 功能可以查看 Optional 的 6 种操作):

@Test
void testTransform() {
    final String text = "看山是山";
    final String reverseText2 = Optional.of(text)
            .map(s -> new StringBuilder(s).reverse().toString())
            .orElse("");
    assertEquals("山是山看", reverseText2);
}

Files 的增强方法:mismatch

在 Java12 中,Files增加了mismatch方法,用于对比两个文件中的不相同字符的位置,如果内容相同,返回-1L,是long类型的。

我们来简单看下怎么用:

@Test
void testMismatch() throws IOException {
    final Path pathA = Files.createFile(Paths.get("a.txt"));
    final Path pathB = Files.createFile(Paths.get("b.txt"));

    // 写入相同内容
    Files.write(pathA, "看山".getBytes(), StandardOpenOption.WRITE);
    Files.write(pathB, "看山".getBytes(), StandardOpenOption.WRITE);

    final long mismatch1 = Files.mismatch(pathA, pathB);
    Assertions.assertEquals(-1L, mismatch1);

    // 追加不同内容
    Files.write(pathA, "是山".getBytes(), StandardOpenOption.APPEND);
    Files.write(pathB, "不是山".getBytes(), StandardOpenOption.APPEND);

    final long mismatch2 = Files.mismatch(pathA, pathB);
    Assertions.assertEquals(6L, mismatch2);

    Files.deleteIfExists(pathA);
    Files.deleteIfExists(pathB);
}

我们可以看到,当第一次在两个文件中写入相同内容,执行mismatch方法返回的是-1L。当第二次追加进去不同的内容后,返回的是6L。之所以是 6,是因为测试代码中使用的字符集是UTF-8,大部分汉子是占用 3 个字符,前两个字相同,从第三个字开始不同,下标从 0 开始,所以开始位置是 6。

Collectors 的增强方法:teeing

我们看下teeing的定义:

public static <T, R1, R2, R> Collector<T, ?, R> teeing(
    Collector<? super T, ?, R1> downstream1,
    Collector<? super T, ?, R2> downstream2,
    BiFunction<? super R1, ? super R2, R> merger
)

这个方法有三个参数,前两个是Collector对象,用于对输入数据进行预处理,第三个参数是BiFunction,用于将前两个处理后的结果作为参数传入BiFunction中,运算得到结果。

我们来看下例子:

@Test
void testTeeing() {
    var result = Stream.of("Sunday", "Monday", "Tuesday", "Wednesday")
            .collect(Collectors.teeing(
                    Collectors.filtering(n -> n.contains("u"), Collectors.toList()),
                    Collectors.filtering(n -> n.contains("n"), Collectors.toList()),
                    (list1, list2) -> List.of(list1, list2)
            ));

    assertEquals(2, result.size());
    assertTrue(isEqualCollection(List.of("Sunday", "Tuesday"), result.get(0)));
    assertTrue(isEqualCollection(List.of("Sunday", "Monday", "Wednesday"), result.get(1)));
}

我们对输入的几个字符串进行过滤,然后将过滤结果组成一个新的队列。

新工具:CompactNumberFormat

这个工具比较好玩,可以对数字进行按需格式化。提供了public static NumberFormat getCompactNumberInstance(Locale locale, NumberFormat.Style formatStyle)方法用于初始化:

  • 第一个参数是指定区域,不同区域展示的结果不同,比如中国展示汉字、美国展示英文;
  • 第二个参数是指定展示结果的模式,分为SHORTLONG,不过对于中文展示,似乎没啥区别。

我们一起看下例子:

@Test
void testFormat() {
    final NumberFormat zhShort = NumberFormat.getCompactNumberInstance(Locale.CHINA, Style.SHORT);
    assertEquals("1 万", zhShort.format(10_000));
    assertEquals("1 兆", zhShort.format(1L << 40));

    final NumberFormat zhLong = NumberFormat.getCompactNumberInstance(Locale.CHINA, Style.LONG);
    assertEquals("1 万", zhLong.format(10_000));
    assertEquals("1 兆", zhLong.format(1L << 40));

    final NumberFormat usShort = NumberFormat.getCompactNumberInstance(Locale.US, Style.SHORT);
    usShort.setMaximumFractionDigits(2);
    assertEquals("10K", usShort.format(10_000));
    assertEquals("1.1T", usShort.format(1L << 40));

    final NumberFormat usLong = NumberFormat.getCompactNumberInstance(Locale.US, Style.LONG);
    usLong.setMaximumFractionDigits(2);
    assertEquals("10 thousand", usLong.format(10_000));
    assertEquals("1.1 trillion", usLong.format(1L << 40));
}

我们也可以继续使用NumberFormat中的方法定义,比如示例中保留小数点后 2 位。

Shenandoah:一个低停顿垃圾收集器

Java12 引入了一个实验阶段的垃圾收集器:Shenandoah,作为一个低停顿的垃圾收集器。

Shenandoah 垃圾收集器是 RedHat 在 2014 年宣布进行的垃圾收集器研究项目,其工作原理是通过与 Java 应用执行线程同时运行来降低停顿时间。简单的说就是,Shenandoah 工作时与应用程序线程并发,通过交换 CPU 并发周期和空间以改善停顿时间,使得垃圾回收器执行线程能够在 Java 线程运行时进行堆压缩,并且标记和整理能够同时进行,因此避免了在大多数 JVM 垃圾收集器中所遇到的问题。

Shenandoah GC

Shenandoah 垃圾回收器的暂停时间与堆大小无关,这意味着无论将堆设置为 200MB 还是 200GB,都将拥有一致的系统暂停时间,不过实际使用性能将取决于实际工作堆的大小和工作负载。

Java12 中 Shenandoah 处于实验阶段,想要使用需要编译时添加--with-jvm-features=shenandoahgc,然后启动时使用-XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC以开启。

后续会补充 Java 中各种垃圾收集器的文章,其中会有介绍 Shenandoah 的,敬请关注公众号「看山的小屋」。如果想要提前了解,欢迎访问https://wiki.openjdk.java.net/display/shenandoah

增加一套基准测试套件

Java12 中添加一套基准测试套件,该基准测试套件基于 JMH(Java Microbenchmark Harness),使开发人员可以轻松运行现有的基准测试并创建新的基准测试,其目标是提供一个稳定且优化的基准。

在这套基准测试套件中包括将近 100 个基准测试的初始集合,并且能够轻松添加新基准、更新基准测试和提高查找已有基准测试的便利性。

微基准套件与 JDK 源代码位于同一个目录中,并且在构建后将生成单个 Jar 文件。它是一个单独的项目,在支持构建期间不会执行,以方便开发人员和其他对构建微基准套件不感兴趣的人在构建时花费比较少的构建时间。

Switch 表达式扩展(预览版)

Switch 语句出现的姿势是条件判断、流程控制组件,与现在很流行的新语言对比,其写法显得非常笨拙,所以 Java 推出了 Switch 表达式语法,可以让我们写出更加简化的代码。这个扩展在 Java12 中作为预览版首次引入,需要在编译时增加-enable-preview开启,在 Java14 中正式提供,功能编号是 JEP 361

比如,我们通过 switch 语法简单计算工作日、休息日,在 Java12 之前需要这样写:

@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());
}

在 Java12 中的 Switch 表达式中,我们可以直接简化:

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

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

是不是很清爽。文末提供的源码中,pom.xml定义的maven.compiler版本写的是14,这是因为 Switch 表达式是 Java14 正式提供,我没有重新编译 Java,所以只能指定 Java14 来实现这个功能代码的演示。

引入 JVM 常量 API

Java12 中引入 JVM 常量 API,用来更容易地对关键类文件和运行时构件的描述信息进行建模,特别是对那些从常量池加载的常量,这是一项非常技术性的变化,能够以更简单、标准的方式处理可加载常量。

具体来说就是java.base模块新增了java.lang.constant包,引入了ConstantDesc接口以及Constable接口。ConstantDesc的子接口包括:

  • ClassDesc:Class 的可加载常量标称描述符;
  • MethodTypeDesc:方法类型常量标称描述符;
  • MethodHandleDesc:方法句柄常量标称描述符;
  • DynamicConstantDesc:动态常量标称描述符。

继续挖坑,这部分内容会在进阶篇再详细介绍,敬请关注公众号「看山的小屋」。

改进 AArch64 实现

Java12 中将只保留一套 AArch64 实现,之前版本中,有两个关于 aarch64 的实现,分别是ope/src/hotspot/cpu/arm以及open/src/hotspot/cpu/aarch64,它们的实现重复了。为了集中精力更好地实现 aarch64,删除了open/src/hotspot/cpu/arm中与 arm64(64-bit Arm platform)实现相关的代码,只保留 32 位 ARM 端口和 64 位 aarch64 的端口。

这样做,可以让开发人员将目标集中在剩下的这个 64 位 ARM 实现上,消除维护两套端口所需的重复工作。

目标聚焦,力量集中。

默认使用类数据共享(CDS)存档

Java10 的新特性 中我们介绍过类数据共享(CDS,Class Data Sharing),其作用是通过构建时生成默认类列表,在运行时使用内存映射,减少 Java 的启动时间和减少动态内存占用量,也能在多个 Java 虚拟机之间共享相同的归档文件,减少运行时的资源占用。

在 Java12 之前,想要使用需要三步走手动开启,到了 Java12,将默认开启 CDS 功能,想要关闭,需要使用参数-Xshare:off

改善 G1 垃圾收集器

能够中止收集

G1 垃圾收集器可以在大内存多处理器的工作场景中提升回收效率,能够满足用户预期降低 STW 停顿时间。

其内部是采用一个高级分析引擎来选择在收集期间要处理的工作量,此选择过程的结果是一组称为 GC 回收集(collection set,CSet)的区域。一旦收集器确定了 GC 回收集 并且 GC 回收、整理工作已经开始,则 G1 收集器必须完成收集集合集的所有区域中的所有活动对象之后才能停止;但是如果收集器选择过大的 GC 回收集,可能会导致 G1 回收器停顿时间超过预期时间。

在 Java12 中,GC 回收集拆分为必需和可选两部分,使 G1 垃圾回收器能中止垃圾回收过程。其中必需处理的部分包括 G1 垃圾收集器不能递增处理的 GC 回收集的部分,同时也可以包含老年代以提高处理效率。在 G1 垃圾回收器完成收集需要必需回收的部分之后,G1 垃圾回收器可以根据剩余时间决定是否停止收集。

向操作系统自动返回未用堆内存

在 Java11 中,G1 仅在进行 Full GC 或并发处理周期时才能向操作系统返还堆内存,但是这两种场景都是 G1 极力避免的,所以如果我们使用 G1 收集器,基本上很难返还 Java 堆内存,这样对于那种周期性执行大量占用内存的应用,会造成比较多的内存浪费。

Java12 中,G1 垃圾收集器将在应用程序不活动期间定期生成或持续循环检查整体 Java 堆使用情况,以便 G1 垃圾收集器能够更及时的将 Java 堆中不使用内存部分返还给操作系统。对于长时间处于空闲状态的应用程序,此项改进将使 JVM 的内存利用率更加高效。

文末总结

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

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

推荐阅读


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

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

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

公众号:看山的小屋