Java10 新特性

你好,我是看山。

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

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

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

概述

从 Java10 开始,Java 版本正式进入每半年一个版本的更新节奏,更新频率加快,小步快跑。

接下来我们瞅瞅 Java10 都更新了哪些比较有意思的功能。

局部变量类型推断

我们都知道 Java 是强类型语言,有着严格的类型限制。想要定义一个变量,必须明确指明变量类型,用Object抬杠的可以离开了。但是从 Java10 开始,我们可以在定义局部变量时使用var限定变量类型,Java 编译器会根据变量的值推断具体的类型。

比如,我们定义一个Map对象:

Map<Integer, String> map = new HashMap<>();

现在我们可以写做:

var idToNameMap = new HashMap<Integer, String>();

这个功能算是 Java 的一次尝鲜,给 Java 语言增加了更多的可能,让我们的代码更加简洁,更加专注于可读性。

需要注意的是,新增的var不会把 Java 变成动态语言,在编译时,编译器会自动推断类型,将其转换为确定的类型,不会在运行时动态变化。

目前var只能用于局部变量,而且等号右侧必须是确定类型的定义,包括:初始化的实例、方法的调用、匿名内部类。

我们再回到刚才的例子:

// 以前的写法
Map<Integer, String> map = new HashMap<>();

// 现在可以这么写
var idToNameMap = new HashMap<Integer, String>();

对于参数的名字,我们可以不在关注类型,可以更多的关注参数的意义,这也是编写可读代码的要求。

这也为我们提出了一些要求,如果是特别长的 Lambda 表达式,还是老老实实的使用明确的类型吧,否则写着写着就迷糊了。

再就是推断类型时没有那么智能,都是基于最明确的推断,比如:

var emptyList = new ArrayList<>();

这个时候推断emptyList的结果是ArrayList<Object>,绝对不会按照我们常用写法推断成List<Object>

如果是匿名内部类,比如:

var obj = new Object() {};

这个时候obj.getClass()可就不是Object.class了,而且匿名内部类的类型了。

所以,小刀虽好,但也要好好用,胡乱用容易误伤。

不可变集合

从 Java9 开始提供不可变集合的实现,Java10 继续扩展。集合是一个容器,作为一个参数传入方法中,我们并不知道方法是否会对容器中的元素进行修改,有了不可变集合,我们就能够在一定程度上进行控制(毕竟对容器中对象的数据进行修改,我们的控制力就没有那么强了)。

针对不可变集合,我们摘取java.util.List的描述(其他的描述都是类似的):

Unmodifiable Lists

The List.of and List.copyOf static factory methods provide a convenient way to create unmodifiable lists. The List instances created by these methods have the following characteristics:

  • They are unmodifiable. Elements cannot be added, removed, or replaced. Calling any mutator method on the List will always cause UnsupportedOperationException to be thrown. However, if the contained elements are themselves mutable, this may cause the List’s contents to appear to change.
  • They disallow null elements. Attempts to create them with null elements result in NullPointerException.
  • They are serializable if all elements are serializable.
  • The order of elements in the list is the same as the order of the provided arguments, or of the elements in the provided array.
  • They are value-based. Callers should make no assumptions about the identity of the returned instances. Factories are free to create new instances or reuse existing ones. Therefore, identity-sensitive operations on these instances (reference equality (==), identity hash code, and synchronization) are unreliable and should be avoided.
  • They are serialized as specified on the Serialized Form page.

简单翻译一下:

  • 这些集合都不可变,元素不能增、减、替换,调用修改方法都会返回UnsupportedOperationException异常。但是,but,如果集合中的元素是可变的,那就控不住了。比如,元素是AtomInteger就没法控制其中的值,集合只是元素不变;如果是String,那集合是整体不变的。
  • 不允许null,会抛出NullPointerException
  • 如果集合中的元素是可序列化的,那集合就能够序列化
  • 集合中元素的顺序是加入的顺序
  • copyOfof这些方法中返回的结果可能使用提前定义好的对象,比如空集合、原集合等。换句话说,在不同调用位置返回了相同对象。所以不要相信==hashCode,也不要对其加锁。

copyOf

java.util.Listjava.util.Mapjava.util.Set这几个接口中都各自添加了一个copyOf静态方法,用来创建不可变集合,最终都会是ImmutableCollections中定义的几个集合实现,与 Java9 中定义的of方法类似。

对于java.util.Mapjava.util.Set,这里有一个优化,如果传入的本身就是不可变的集合,将直接返回传入的参数,代码如下:

static <E> Set<E> copyOf(Collection<? extends E> coll) {
    if (coll instanceof ImmutableCollections.AbstractImmutableSet) {
        return (Set<E>)coll;
    } else {
        return (Set<E>)Set.of(new HashSet<>(coll).toArray());
    }
}

toUnmodifiableList、toUnmodifiableSet、toUnmodifiableMap

Java10 很贴心的提供了Stream中的操作,我们直接创建不可变集合了。比如:

Stream.of("1", "2", "3", "4", "5")
    .map(x -> "id: " + x)
    .collect(Collectors.toUnmodifiableList());

toUnmodifiableListtoUnmodifiableSettoUnmodifiableMap的用法与toListtoSettoMap没有太多区别,差别在于返回的是不可变集合。

Optional 族增加 orElseThrow 方法

这里说的 Optional 族包括OptionalOptionalIntOptionalLongOptionalDouble几个实现。以前有一个orElseThrow(Supplier<? extends X> exceptionSupplier)方法,用于获取不到数据时,抛出exceptionSupplier中定义的异常。

我们会写成:

Stream.of("1", "2", "3", "4", "5")
    .map(x -> "id: " + x)
    .findAny()
    .orElseThrow(() -> new NoSuchElementException("No value present"));

优点是我们可以自定义自己的异常以及异常信息。有时候,我们不关心具体的异常和异常信息,这个时候 Java10 中的新增的orElseThrow方法就派上用场了:

Stream.of("1", "2", "3", "4", "5")
    .map(x -> "id: " + x)
    .findAny()
    .orElseThrow();

此时如果元素为空,将抛出NoSuchElementException异常。

性能优化

Java 当年在性能方面一直被诟病,中间隔着一层虚拟机,实现跨平台运行的功能同时,也致使其执行性能不如 C 语言。所以,Java 一直在性能方面投入大量精力。

我们看看 Java10 中都有哪些优化点。

G1 实现并行 Full GC 算法

从 Java9 开始,G1 已经转正,成为默认的垃圾收集器。不过在 Full GC 时,G1 还是采用的单线程串行标记压缩算法,这样 STW 时间会比较长。到 Java10,Full GC 实现了并行标记压缩算法,明显缩短 STW 时间。

应用程序类数据共享(AppCDS)

CDS(Class-Data Sharing,类数据共享)是在 Java5 引入的一种类预处理方式,可以将一组类共享到一个归档文件中,在运行时通过内存映射加载类,这样做可以减少启动时间。同时在多个 JVM 之间实现同享同一个归档文件,减少动态内存占用。

但是 CDS 有一个限制,就是只能是 Bootstrap ClassLoader 使用,这样就将功能限制了类的范围。在 Java10 中,将这个功能扩展到了系统类加载器(System ClassLoader,或者成为应用类加载器,Application ClassLoader)、内置的平台类加载器(Platform ClassLoader),或者是自定义的类加载器。这样就将功能扩展到了应用类。

想要使用这个功能的话,总共分三步:

总共分三步

  1. 打包
    我们把hello.jar中的HelloWorld类使用的类添加到hello.lst中:
$ java -Xshare:off -XX:+UseAppCDS -XX:DumpLoadedClassList=hello.lst \ 
   -cp hello.jar HelloWorld
  1. 创建 AppCDS 归档
    接下来使用hello.lst中的内容创建 AppCDS 文件hello.jsa
$ java -Xshare:dump -XX:+UseAppCDS -XX:SharedClassListFile=hello.lst \
   -XX:SharedArchiveFile=hello.jsa -cp hello.jar
  1. 使用 AppCDS 归档
    最后是使用hello.jsa启动HelloWorld
$ java -Xshare:on -XX:+UseAppCDS -XX:SharedArchiveFile=hello.jsa \
   -cp hello.jar HelloWorld

基于 Java 的 JIT 编译器 Graal

Graal 是使用 Java 编写的与 HotSpot JVM 集成的动态编译器,专注于高性能和可扩展性。是从 JDK9 引入的实验性 AOT 编译器的基础。

在 JDK10 中,我们可以在 Linux/x64 平台将 Graal 作为 JIT 编译器使用。开启命令如下:

-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler

需要注意的是,这是实验特性,相当于公测阶段,可能在某些场景下,性能不如现有的 JIT 编译器。

支持容器

容器化是目前的趋势,在之前,由于 JVM 不能够感知容器,在同一个主机上部署多个虚拟机时,会造成内存占用、CPU 抢占等问题,这点也成为了很多大会上抨击 Java 语言不适合容器时代的一个点。

现在好了,JVM 可以感知容器了。只是暂时还只支持 Linux 系统(so what,其他平台也还没有用过)。这个功能默认开启,不想使用可以手动关闭:

-XX:-UseContainerSupport

我们还可以手动指定 CPU 核数:

-XX:ActiveProcessorCount=1

还有三个可以控制内存的使用量:

-XX:InitialRAMPercentage
-XX:MaxRAMPercentage
-XX:MinRAMPercentage

就目前来看,这部分还可以继续完善,相信只是时间问题。

根证书认证

自 Java9 起在 keytool 中加入参数 -cacerts,可以查看当前 JDK 管理的根证书。而 Java9 中 cacerts 目录为空,这样就会给开发者带来很多不便。从 Java10 开始,将会在 JDK 中提供一套默认的 CA 根证书。

作为 JDK 一部分的 cacerts 密钥库旨在包含一组能够用于在各种安全协议的证书链中建立信任的根证书。在 Java10 之前,cacerts 密钥库是空的,默认情况下,关键安全组件(如 TLS)是不起作用的,开发人员需要手动添加一组根证书来使用这些验证。

Java10 中,Oracle 开放了根证书源码,可以让 OpenJDK 构建对开发人员更有吸引力,并减少这些构建与 Oracle JDK 构建之间的差异。

启用和删除的功能

有增有减,这样才能够保证 Java 的与时共进。

命令行工具和某些参数

  • 移除javah命令,这个命令用于创建 native 方法所需的 C 的头文件和资源文件的,使用javac -h替代。
  • 移除policytool工具,这个工具用于创建和管理策略文件。可以直接还使用文本编辑器代替。
  • 删除java -Xprof参数,这个参数本来是用于评测正在运行的程序,并将评测数据发送到标准输出。可以使用jmap代替。

某些 API

java.security.acl包标记为过期,标记参数forRemovaltrue,将在未来版本中删除。目前,这个包内的功能已经被java.security.Policy取代。java.security包中的CertificateIdentityIdentityScopeSigner的标记参数forRemoval也是true。这些都将在后续版本中删除。

基于时间的版本发布模式

从 Java10 开始,Java 正式进入每半年一个版本的更新节奏,主要改动如下:

  1. 每 6 个月发布一组新特性;
  2. 长期支持版(LTS)将支持 3 年,其他版本支持 6 个月;
  3. Java11、Java17 是长期支持版;
  4. 采用$FEATURE.$INTERIM.$UPDATE.$PATCH命名机制:$FEATURE,每次版本发布加 1,不考虑具体的版本内容;$INTERIM,中间版本号,在大版本中间发布的,包含问题修复和增强的版本,不会引入非兼容性修改;$PATCH用于快速打补丁的。

文末总结

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

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

推荐阅读


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

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

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

公众号:看山的小屋