Java11 的新特性

你好,我是看山。

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

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

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

概述

Java11 是 2018 年 9 月发布的,是自 Java8 之后第一个长期支持版(long-term support,LTS)。相比于其他版本 6 个月维护期,长期支持版的维护期是 3 年。

长期支持版的更新会比较多,而且都是相对稳定的更新。今天我们就一起看看 Java11 都有哪些喜人的变化:增强的 API、全新的 HTTP 客户端、基于嵌套关系的访问控制优化、低开销的堆性能采用工具、ZGC、Epsilon 垃圾收集器、飞行记录器等。

增强 String

首先说下String中新增的方法:repeatstripstripLeadingstripTrailingisBlanklines。这些方法还是挺有用的,以前我们可能需要借助第三方类库(比如 Apache 出品的 commons-lang)中的工具类,现在可以直接使用嫡亲方法了。

repeat

repeat是实例方法,顾名思义,这个方法是返回给定字符串的重复值的,参数是int类型,传参的时候需要注意:

  • 如果重复次数小于 0 会抛出IllegalArgumentException异常;
  • 如果重复次数为 0 或者字符串本身是空字符串,将返回空字符串;
  • 如果重复次数为 1 直接返回本身;
  • 如果字符串重复指定次数后,长度超过Integer.MAX_VALUE,会抛出OutOfMemoryError错误。

用法很简单:

@Test
void testRepeat() {
    String output = "foo ".repeat(2) + "bar";
    assertEquals("foo foo bar", output);
}

小而美的一个工具方法。

strip、stripLeading、stripTrailing

strip方法算是trim方法的增强版,trim方法可以删除字符串两侧的空白字符(空格、tab 键、换行符),但是对于Unicode的空白字符无能为力,strip补足这一短板。

用起来是这样的:

@Test
void testTrip() {
    final String output = "\n\t  hello   \u2005".strip();
    assertEquals("hello", output);

    final String trimOutput = "\n\t  hello   \u2005".trim();
    assertEquals("hello   \u2005", trimOutput);
}

对比一下可以看到,trim方法的清理功能稍弱。

stripLeadingstripTrailingstrip类似,区别是一个清理头,一个清理尾。用法如下:

@Test
void testTripLeading() {
    final String output = "\n\t  hello   \u2005".stripLeading();
    assertEquals("hello   \u2005", output);
}

@Test
void testTripTrailing() {
    final String output = "\n\t  hello   \u2005".stripTrailing();
    assertEquals("\n\t  hello", output);
}

isBlank

这个方法是用于判断字符串是否都是空白字符,除了空格、tab 键、换行符,也包括Unicode的空白字符。

用法很简单:

@Test
void testIsBlank() {
    assertTrue("\n\t\u2005".isBlank());
}

lines

最后这个方法是将字符串转化为字符串Stream类型,字符串分隔依据是换行符:\n\r\r\n,用法如下:

@Test
void testLines() {
    final String multiline = "This is\n \na multiline\nstring.";
    final String output = multiline.lines()
            .filter(Predicate.not(String::isBlank))
            .collect(Collectors.joining(" "));
    assertEquals("This is a multiline string.", output);
}

增强文件读写

本次更新在Files中增加了两个方法:readStringwriteStringwriteString作用是将指定字符串写入文件,readString作用是从文件中读出内容到字符串。是一个对Files工具类的增强,封装了对输出流、字节等内容的操作。

用法比较简单:

@Test
void testReadWriteString() throws IOException {
    final Path tmpPath = Path.of("./");
    final Path tempFile = Files.createTempFile(tmpPath, "demo", ".txt");
    final Path filePath = Files.writeString(tempFile, "看山 howardliu.cn\n 公众号:看山的小屋");
    assertEquals(tempFile, filePath);

    final String fileContent = Files.readString(filePath);
    assertEquals("看山 howardliu.cn\n 公众号:看山的小屋", fileContent);

    Files.deleteIfExists(filePath);
}

readStringwriteString还可以指定字符集,不指定默认使用StandardCharsets.UTF_8字符集,可以应对大部分场景了。

增强集合的数组操作

java.util.Collection提供了集合转数组的方法有两个:

  • Object[] toArray():可以直接转数组,但是转换后是Object类型,后续使用的时候,需要强转,太不优雅了;
  • <T> T[] toArray(T[] a):传入一个指定类型的数组,一般会有另种实现:
    • 一是,如果传入数组长度小于列表长度,会借助Arrays.copyOf创建列表长度的数组,这个数组与传入数组参数没有关系
    • 二是,如果传入数组长度大于等于列表长度,会借助System.arraycopy将列表写入数组,超过长度的数组元素置为null

我们一般这样用:

@Test
void testArray() {
    final List<String> vars = Arrays.asList("1", "2", "3");
    final Object[] objArray = vars.toArray();
    final String[] strArray = vars.toArray(new String[0]);
    Assertions.assertTrue(Arrays.asList(strArray).contains("1"));
    Assertions.assertTrue(Arrays.asList(strArray).contains("2"));
    Assertions.assertTrue(Arrays.asList(strArray).contains("3"));
}

在 Java11 中,又新增了一种实现,相当于对<T> T[] toArray(T[] a)做了增强,其源码是:

default <T> T[] toArray(IntFunction<T[]> generator) {
    return toArray(generator.apply(0));
}

可以看到,是通过传入一个IntFunction类型的函数,然后调用<T> T[] toArray(T[] a)创建数组,其实是采用了我们常用的给toArray传入空数组的方式,用法如下:

@Test
void testArray() {
    final List<String> vars = Arrays.asList("1", "2", "3");
    final String[] strArray2 = vars.toArray(String[]::new);
    Assertions.assertTrue(Arrays.asList(strArray2).contains("1"));
    Assertions.assertTrue(Arrays.asList(strArray2).contains("2"));
    Assertions.assertTrue(Arrays.asList(strArray2).contains("3"));
}

从使用上,似乎没有太多的提升,但是写法上,使用了函数式编程,是不是很优雅。

优雅的假笑

增强函数 Predicate

这个也是方法增强,在以前,我们在Stream中的filter方法判断否的时候,一般需要!运算,比如我们想要找到字符串列表中的数字,可以这样写:

final List<String> list = Arrays.asList("1", "a");
final List<String> nums = list.stream()
        .filter(NumberUtils::isDigits)
        .collect(Collectors.toList());
Assertions.assertEquals(1, nums.size());
Assertions.assertTrue(nums.contains("1"));

想要找到非数字的,filter方法写的就会用到!非操作:

final List<String> notNums = list.stream()
        .filter(x -> !NumberUtils.isDigits(x))
        .collect(Collectors.toList());
Assertions.assertEquals(1, notNums.size());
Assertions.assertTrue(notNums.contains("a"));

Java11 中为Predicate增加not方法,可以更加简单的实现非操作:

final List<String> notNums2 = list.stream()
        .filter(Predicate.not(NumberUtils::isDigits))
        .collect(Collectors.toList());
Assertions.assertEquals(1, notNums2.size());
Assertions.assertTrue(notNums2.contains("a"));

有些教程还会推崇静态引入,比如在头部使用import static java.util.function.Predicate.not,这样在函数式编程时,可以写更少的代码,语义更强,比如:

final List<String> notNums2 = list.stream()
        .filter(not(NumberUtils::isDigits))
        .collect(toList());

喜好随人,没有优劣。

Lambda 中的局部变量

局部变量是 Java10 中增加的特性,具体可以查看 Java10 的新特性 中的介绍,但是不支持在 Lambda 中使用局部变量。

在 Lambda 中,我们可以这样操作:

(String s1, String s2) -> s1 + s2

也可以这样:

(s1, s2) -> s1 + s2

到 Java11 之后,我们还能这样:

(var s1, var s2) -> s1 + s2

单纯从语法上,似乎没啥特点,但是如果再加上一些别的用法,比如:

(@Nonnull var s1, @Nullable var s2) -> s1 + s2

是不是就能看出差别了,我们可以有如下的操作:

@Test
void testLocalVariable() {
    final List<String> sampleList = Arrays.asList("Hello", "World");
    final String resultString = sampleList.stream()
            .map((@NotNull var x) -> x.toUpperCase())
            .collect(Collectors.joining(", "));
    Assertions.assertEquals("HELLO, WORLD", resultString);
}

不过,这里还是有一些限制,比如:

如果是多个参数,不能有的使用var修饰,有的不指定类型:

// 错误写法
(var s1, s2) -> s1 + s2

或者,不能混合使用,一个使用var修饰,一个使用明确的类型:

// 错误写法
(var s1, String s2) -> s1 + s2

如果是单个参数,如果是单行操作,我们可以不写{},但是使用var修饰的时候,就不能省略{}了:

// 错误写法
var s1 -> s1.toUpperCase()

还是有一些限制的,我们在便利的同时,需要符合一定的约束。自由和规范不冲突。

转正的 HTTP 客户端

Java9 的新特性 中说过,Java 中有一个全新的 HTTP 客户端,当时还在孵化模块中,到 Java11 可以正式使用了。

新客户端用法简单、性能可靠,而且支持功能也多。我们先简单看下使用:

@Test
void testHttpClient() throws IOException, InterruptedException {
    final HttpClient httpClient = HttpClient.newBuilder()
            .version(HttpClient.Version.HTTP_2)
            .connectTimeout(Duration.ofSeconds(20))
            .build();
    final HttpRequest httpRequest = HttpRequest.newBuilder()
            .GET()
            .uri(URI.create("https://www.howardliu.cn/robots.txt"))
            .build();
    final HttpResponse<String> httpResponse = httpClient.send(httpRequest, BodyHandlers.ofString());
    final String responseBody = httpResponse.body();
    assertTrue(responseBody.contains("Allow"));
}

基于嵌套关系的访问控制优化

这部分是遗留的技术债务,从 Java1.1 开始,到 Java11 修复,属于 Valhalla 项目的一部分,我们在 Java11 中基于嵌套关系的访问控制优化 一文中有详细解释,这里就不再赘述了。

增强 java 命令

在 Java11 之前,想要运行源文件,需要先通过javac命令编译,然后使用java命令运行,先可以直接使用java运行了:

$ java HelloWorld.java
Hello Java 11!

动态类文件常量

为了使 JVM 对动态语言更具吸引力,Java 指令集引入了 invokedynamic。

不过 Java 开发人员通常不会注意到此功能,因为它隐藏在 Java 字节代码中。通过使用 invokedynamic,可以延迟方法调用的绑定。例如,Java 语言使用该技术来实现 Lambda 表达式,这些表达式仅在首次使用时才显示出来。这样做,invokedynamic 已经演变成一种必不可少的语言功能。

Java 11 引入了类似的机制,扩展了 Java 文件格式,以支持新的常量池:CONSTANT_Dynamic,它在初始化的时候,像 invokedynamic 指令生成代理方法一样,委托给 bootstrap 方法进行初始化创建,对上层软件没有很大的影响,降低开发新形式的可实现类文件约束带来的成本和干扰。

此功能可提高性能,并面向语言设计人员和编译器实现人员。

低开销的堆性能采用工具

Java 11 中提供一种低开销的 Java 堆分配采样方法,能够得到堆分配的 Java 对象信息,并且能够通过 JVMTI 访问堆信息。

引入这个低开销内存分析工具是为了达到如下目的:

  • 足够低的开销,可以默认且一直开启;
  • 能通过定义好的程序接口访问;
  • 能够对所有堆分配区域进行采样;
  • 能给出正在和未被使用的 Java 对象信息。

对用户来说,了解堆中内存分布是非常重要的,特别是遇到生产环境中出现的高 CPU、高内存占用率的情况。目前有一些已经开源的工具,允许用户分析应用程序中的堆使用情况,比如:Java Flight Recorder、jmap、YourKit 以及 VisualVM tools.。但是这些工具都有一个明显的不足之处:无法得到对象的分配位置,headp dump 以及 heap histogram 中都没有包含对象分配的具体信息,但是这些信息对于调试内存问题至关重要,因为它能够告诉开发人员他们的代码中发生的高内存分配的确切位置,并根据实际源码来分析具体问题,这也是 Java 11 中引入这种低开销堆分配采样方法的原因。

ZGC

ZGC 是一个可伸缩、低延迟的垃圾收集器,性能由于 G1 收集器,从 Java11 开始可以在 Linux/x64 平台体验,全平台支持是从 Java17 开始。详细介绍可以从https://wiki.openjdk.java.net/display/zgc/Main查看。

在 Java11 中尚处于试验阶段,没有包含在 JDK 构建中,想要启用,需要在 JDK 编译时添加参数--with-jvm-features=zgc。显式启用了 ZGC 之后,我们可以使用构建好的 JDK 启动,需要添加参数-XX:+UnlockExperimentalVMOptions -XX:+UseZGC

ZGC 有下面几个目标:

  • GC 停顿时间不超过 10ms
  • 可以处理从几百 MB 的小堆,到几个 TB 的大堆
  • 与 G1 回收算法相比,应用吞吐能力不会下降超过 15%
  • 为未来的 GC 特性和优化有色指针和负载屏障奠定基础

https://openjdk.java.net/jeps/333给出的数据可以看出来,在 128G 堆大小的测试中,ZGC 优势明显,找了一张网上的图片:

SPECjbb 2015 的基准测试

这里预告一下,Java12 中也增加了一个实现阶段的垃圾收集器 Shenandoah,到时候咱们看一下。

改进 Aarch64 指令集

Java 11 优化了 ARM64 或 Arch64 处理器上现有的字符串和数组内部函数。还为java.lang.Mathsincoslog方法实现了新的内部函数。

我们像其他函数一样使用内在函数,但是,编译器会以特殊的方式处理内部函数,将使用 CPU 体系结构特定的汇编代码来提高性能。可以关注一下HotSpotIntrinsicCandidate这个注解。

Epsilon 垃圾收集器

Java11 引入了一个新的实验性垃圾收集器:Epsilon。Epsilon 垃圾收集器提供一个完全消极的 GC 实现,分配有限的内存资源,最大限度的降低内存占用和内存吞吐延迟时间,适用于模拟内存不足错误的场景。

Epsilon 垃圾收集器有几个使用场景:

  • 性能测试:无操作的垃圾收集器可以过滤因为收集器自身原因造成的性能损失;
  • 内存压力测试:可以用于验证分配内存的阈值;
  • VM 接口测试:以 VM 开发视角,有一个简单的 GC 实现,有助于理解 VM-GC 的最小接口实现。它也用于证明 VM-GC 接口的健全性;
  • 存活极短的任务:这种生命周期极短的任务,需要实现快速启动、快速释放资源的特性。开发者知道这种任务的内存阈值是多少,很大概率上,任务存活周期内,不会触发垃圾回收,就需要一个什么也不干的收集器站着位置就行。

可以通过-XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC参数开启。

飞行记录器(Flight Recorder)

飞行记录器(Flight Recorder)可是个好东西,之前是 Oracle JDK 中的一个商用产品,现已在 Open JDK 中开源。这是一种低开销的事件信息收集框架,主要用于对应用程序和 JVM 进行故障检查、分析。

飞行记录器记录的主要数据源于应用程序、JVM 和操作系统,这些事件信息保存在单独的事件记录文件中,故障发生后,能够从事件记录文件中提取出有用信息对故障进行分析。有些类似于飞机上的黑匣子。

飞机上的黑匣子

比如,我们可以使用以下参数开启一个时长为 120 秒的记录:

-XX:StartFlightRecording=duration=120s,settings=profile,filename=recording.jfr

生成的文件可以使用 JMC 工具可视化查看,也可以自己写代码通过RecordedEvent解析。不过嘛,有可视化的,干嘛还要自己敲代码呢?

我们也可以在运行时通过jcmd命令启动记录:

$ jcmd <pid> JFR.start
$ jcmd <pid> JFR.dump filename=recording.jfr
$ jcmd <pid> JFR.stop

收到监控,想推广一下之前写的开源监控组件 Cynomys,源码在https://github.com/howardliu-cn/cynomys,里面包含通过 Netty 实现的 RPC 框架、javaagent 实现的探针、使用 javassist 操作字节码、JMX 实现 JVM 内部监控等,可以对操作系统、网络、JVM、请求、SQL 等内容进行监控。

移除或过期组件

社会在发展,技术在进步。又有一些功能或组件不合时宜,要么移除、要么标记过期。标记过期的最好不要再用了,不知道哪天就会被移除,想要升级依赖反而麻烦。

  • JavaEE 和 CORBA:单独的 JavaEE 版本可以从第三方站点获取,所以在 JavaSEO 中不再包含。从 Java9 开始,JavaEE 和 CORBA 模块已经标记为过期,到 Java11 就完全移除。
  • JMC 从 JDK 中移除,可以单独下载。
  • JavaFX 也是这样,从 JDK 模块从中移除,需要单独引入。
  • Nashorn JavaScript 引擎标记为废弃
  • Jar 包的 Pack200 压缩方案标记为废弃

其他小改动

  • 实现了新的 ChaCha20 和 ChaCha20-Poly1305 加密算法,取代不安全的 RC4。
  • 使用 Curve25519 和 Curve448 支持加密密钥协议,以取代现有的 ECDH 方案
  • 升级 TLS 版本到 1.3,提升了安全性和性能
  • 支持 Unicode 10, 带来了更多的字符、符号和表情符号

文末总结

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

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

推荐阅读


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

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

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

公众号:看山的小屋