Java 16中的新特性

2023/06/09

1. 概述

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

在本文中,我们将探索其中的一些新功能。

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

作为对接口中默认方法的增强,随着Java 16的发布,java.lang.reflect.InvocationHandler支持使用反射通过动态代理调用接口的默认方法

为了说明这一点,让我们看一个简单的默认方法示例:

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

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

@Test
void givenAnInterfaceWithDefaulMethod_whenCreatingProxyInstance_thenCanInvokeDefaultMethod() throws Exception {
	Object proxy = Proxy.newProxyInstance(getSystemClassLoader(), new Class<?>[]{HelloWorld.class},
		(prox, method, args) -> {
			if (method.isDefault()) {
				return InvocationHandler.invokeDefault(prox, method, args);
			}
			return method.invoke(prox, 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");

我们得到的输出不是“3pm”,而是“3 in the afternoon”。我们还可以使用“B”、“ BBBB ”或“BBBBB” DateTimeFormatter模式分别用于短、全和窄风格。

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

在上面的代码中,我们通过新旧两种方法将String集合收集成另一个集合元素。

5. Vector 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);

在基于向量的代码中,我们做的第一件事是使用IntVector类的静态工厂方法fromArray从我们的输入数组创建两个IntVector,第一个参数是向量的大小,后面是数组和偏移量(这里设置为0)。这里最重要的是我们达到128位的向量大小,在Java中,每个int占用4个字节。

由于我们有一个包含4个整数的输入数组,因此需要128位来存储。我们的单个Vector可以存储整个数组。

在某些架构上,编译器将能够优化字节码,将计算从4个周期减少到只有1个周期。这些优化有利于机器学习和密码学等领域。

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

6. Record(JEP-395)

Records是在Java 14中引入的,Java 16带来了一些增量变化

记录与枚举的相似之处在于它们是一种受限制的类形式,定义记录是定义不可变数据保存对象的一种简洁方式。

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中创建简单的数据保存类需要大量样板代码,这可能很麻烦,并且可能会导致在开发人员没有提供所有必要方法(例如equals和hashCode)时出现错误。

同样,有时开发人员会跳过创建适当的不可变类的必要步骤。有时我们最终会重用一个通用类,而不是为每个不同的用例定义一个专门的类。

大多数现代IDE都提供了自动生成代码(例如setter、getter、构造函数等)的功能,这有助于缓解这些问题并减少开发人员编写代码的开销。但是,Records提供了一种内置机制来减少样板代码并可以达到相同的效果。

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关键字与permits关键字结合使用,来准确确定允许哪些类实现JungleAnimal接口。在我们的例子中为Monkey和Snake。

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

  • sealed:意味着他们必须使用permits关键字定义允许从它继承的类
  • 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块,因为密封类只允许Monkey和Snake这两种可能的子类型。

8.2 Java 16中密封类的新增功能

Java 16中对密封类进行了一些补充,以下是Java 16对密封类引入的更改:

  • Java语言将sealed、non-sealed和permits识别为上下文关键字(类似于abstract和extends)
  • 限制创建作为密封类子类的局部类的能力(类似于不能创建密封类的匿名类)。
  • 转换密封类和从密封类派生的类时进行更严格的检查

9. 其他变化

延续Java 15版本中的JEP-383,外部链接器API提供了一种灵活的方式来访问主机上的本机代码。最初是为了实现C语言的互操作性,未来可能会适配C++或Fortran等其他语言。此功能的目标是最终取代Java Native Interface

另一个重要的变化是JDK内部结构现在默认是强封装的,自Java 9以来,这些都是可访问的。但是,现在需要向JVM提供参数–illegal-access=permit。这将影响所有当前直接使用JDK内部结构并忽略警告消息的库和应用程序(尤其是在测试时)。

10. 总结

在本文中,我们介绍了作为增量Java 16发行版的一部分引入的一些功能和更改,Java 16中的完整更改列表在JDK发行说明中可以找到。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章