Contents

Java 16中的新功能

1. 概述

Java 16 于 2021 年 3 月 16 日发布,是基于Java 15 构建的最新短期增量版本。此版本带有一些有趣的功能,例如记录和密封类。 在本文中,我们将探讨其中的一些新功能。

2. 从代理实例调用默认方法 (JDK-8159746)

作为对接口中默认方法的增强,随着 Java 16 的发布,增加了对java.lang.reflect.InvocationHandler 通过 使用反射的动态代理 调用 接口的默认方法的支持。 为了说明这一点,让我们看一个简单的默认方法示例:

interface HelloWorld {
    default String hello() {
        return "world";
    }
}

通过此增强功能,我们可以使用反射在该接口的代理上调用默认方法:

Object proxy = Proxy.newProxyInstance(getSystemClassLoader(), new Class<?>[] { HelloWorld.class },
    (prox, method, args) -> {
        if (method.isDefault()) {
            return InvocationHandler.invokeDefault(prox, method, args);
        }
        // ...
    }
);
Method method = proxy.getClass().getMethod("hello");
assertThat(method.invoke(proxy)).isEqualTo("world");

3. 日间支持 (JDK-8247781)

DateTimeFormatter的新增功能 是时段符号“ B ”,它提供了 am/pm 格式的替代方案:

LocalTime date = LocalTime.parse("15:25:08.690791");
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("h B");
assertThat(date.format(formatter)).isEqualTo("3 in the afternoon");

我们得到的不是“下午 3 点”之类的输出,而是“**下午3 点”的输出。我们还可以分别使用“ B ”、“ BBBB ”或“ BBBBBDateTimeFormatter 模式来表示短样式、完整样式和窄样式。

4. 添加Stream.toList方法 (JDK-8180352)

目的是使用一些常用的收集器来减少样板,例如Collectors.toList 和Collectors.toSet

List<String> integersAsString = Arrays.asList("1", "2", "3");
List<Integer> ints = integersAsString.stream().map(Integer::parseInt).collect(Collectors.toList());
List<Integer> intsEquivalent = integersAsString.stream().map(Integer::parseInt).toList();

我们的ints示例以旧方式工作,但intsEquivalent具有相同的结果并且更简洁。

5. 矢量 API 孵化器 (JEP-338)

Vector API 处于 Java 16 的初始孵化阶段。此 API 的想法是提供一种矢量计算方法,最终能够比传统的标量计算方法更优化地执行(在支持 CPU 架构上)。

让我们看看传统上如何将两个数组相乘:

int[] a = {1, 2, 3, 4};
int[] b = {5, 6, 7, 8};
var c = new int[a.length];
for (int i = 0; i < a.length; i++) {
    c[i] = a[i] * b[i];
}

对于长度为 4 的数组,此标量计算示例将在 4 个周期内执行。现在,让我们看看等效的基于向量的计算:

int[] a = {1, 2, 3, 4};
int[] b = {5, 6, 7, 8};
var vectorA = IntVector.fromArray(IntVector.SPECIES_128, a, 0);
var vectorB = IntVector.fromArray(IntVector.SPECIES_128, b, 0);
var vectorC = vectorA.mul(vectorB);
vectorC.intoArray(c, 0);

我们在基于向量的代码中做的第一件事是使用此类fromArray的静态工厂方法从我们的输入数组创建两个IntVector  。第一个参数是向量的大小,后面是数组和偏移量(这里设置为0)。这里最重要的是我们要达到 128 位的向量的大小。在 Java 中,每个int需要 4 个字节来保存。 由于我们有一个 4 个整数的输入数组,因此需要 128 位来存储。我们的单个Vector可以存储整个数组。 在某些架构上,编译器将能够优化字节码以将计算从 4 个周期减少到仅 1 个周期。这些优化有利于机器学习和密码学等领域。

我们应该注意到,处于孵化阶段意味着这个 Vector API 可能会随着新版本的发布而改变。

6. 记录 (JEP-395)

record 是在 Java 14 中引入的。Java 16 带来了一些增量变化 。 记录类似于enum,因为它们是类的受限形式。定义record是定义不可变数据保存对象的一种简洁方式。

6.1. 没有记录的例子

首先,让我们定义一个Book 类:

public final class Book {
    private final String title;
    private final String author;
    private final String isbn;
    public Book(String title, String author, String isbn) {
        this.title = title;
        this.author = author;
        this.isbn = isbn;
    }
    public String getTitle() {
        return title;
    }
    public String getAuthor() {
        return author;
    }
    public String getIsbn() {
        return isbn;
    }
    @Override
    public boolean equals(Object o) {
        // ...
    }
    @Override
    public int hashCode() {
        return Objects.hash(title, author, isbn);
    }
}

在 Java 中创建简单的数据保存类需要大量的样板代码。这可能很麻烦,并导致开发人员不提供所有必要的方法(例如equalshashCode )的错误。 同样,有时开发人员会跳过创建适当的不可变类 的必要步骤。有时我们最终会重用一个通用类,而不是为每个不同的用例定义一个专门的类。 大多数现代 IDE 提供了自动生成代码(例如 setter、getter、构造函数等)的能力,这有助于缓解这些问题并减少开发人员编写代码的开销。但是,记录提供了一种内置机制来减少样板代码并创建相同的结果。

6.2. 记录示例

这是作为记录重写的Book

public record Book(String title, String author, String isbn) {
}

通过使用record关键字,我们将Book类减少到两行。这使得它更容易,更不容易出错。

6.3. Java 16 中对记录的新添加

随着 Java 16 的发布,我们现在可以将记录定义为内部类的类成员。这是由于放宽了在JEP-384 下作为 Java 15 增量发布的一部分而遗漏的限制:

class OuterClass {
    class InnerClass {
        Book book = new Book("Title", "author", "isbn");
    }
}

7. instanceof的模式匹配(JEP-394)

从 Java 16 开始添加了instanceof 关键字 的模式匹配。 以前我们可能会这样写代码:

Object obj = "TEST";
if (obj instanceof String) {
    String t = (String) obj;
    // do some logic...
}

此代码必须首先检查obj的实例,然后将对象转换为String并将其分配给新变量t,而不是仅仅关注应用程序所需的逻辑。 随着模式匹配的引入,我们可以重写这段代码:

Object obj = "TEST";
if (obj instanceof String t) {
    // do some logic
}

我们现在可以声明一个变量——在这个实例中是t——作为instanceof检查的一部分。

8. 密封课程 (JEP-397)

密封类首先在 Java 15 中引入,它提供了一种机制来确定允许哪些子类扩展或实现父类或接口。

8.1. 例子

让我们通过定义一个接口和两个实现类来说明这一点:

public sealed interface JungleAnimal permits Monkey, Snake  {
}
public final class Monkey implements JungleAnimal {
}
public non-sealed class Snake implements JungleAnimal {
}

sealed关键字与permit 关键字一起使用,以准确确定允许哪些类实现此接口。在我们的示例中,这是MonkeySnake

密封类的所有继承类必须用以下之一标记:

  • sealed的——这意味着他们必须使用 permit 关键字定义允许从它继承的类。
  • final - 防止任何进一步的子类
  • non-sealed——允许任何类能够继承它。

密封类的一个显着好处是它们允许详尽的模式匹配检查,而不需要对所有未覆盖的情况进行捕获。例如,使用我们定义的类,我们可以有逻辑覆盖所有可能的JungleAnimal子类:

JungleAnimal j = // some JungleAnimal instance
if (j instanceof Monkey m) {
    // do logic
} else if (j instanceof Snake s) {
    // do logic
}

我们不需要else块,因为密封类只允许MonkeySnake这两种可能的子类型。

8.2. Java 16 中密封类的新添加

Java 16 中对密封类进行了一些添加 。这些是 Java 16 对密封类引入的更改:

  • Java 语言将sealednon-sealedpermit识别为上下文关键字(类似于abstractextends
  • 限制创建作为密封类的子类的本地类的能力(类似于无法创建密封类的匿名类)。
  • 转换密封类和从密封类派生的类时进行更严格的检查

9. 其他变更

从 Java 15 版本中的 JEP-383 开始,外部链接器 API 提供了一种灵活的方式来访问主机上的本机代码。最初,为了 C 语言的互操作性,将来可能会适应其他语言,例如 C++ 或 Fortran。此功能的目标是最终替换Java Native Interface 。 另一个重要的变化是 JDK 内部现在默认被强封装 。这些从 Java 9 开始就可以访问了。但是,现在 JVM 需要参数*–illegal-access=permit*。这将影响当前直接使用 JDK 内部的所有库和应用程序(尤其是在测试方面),并且直接忽略警告消息。