就 Java 16 中的新特性而言,我将分享 Stream API 中一项讨喜的更新,然后紧张关注措辞变动部分。
从 Stream 到 ListList<String> features = Stream.of("Records", "Pattern Matching", "Sealed Classes") .map(String::toLowerCase) .filter(s -> s.contains(" ")) .collect(Collectors.toList());
复制代码
如果你习气利用 Java Stream API,那么该当会很熟习上面这个代码段。

这段代码里有一个包含一些字符串的流。我们在它上面映射一个函数,然后过滤这个流。
末了,我们将流归天为一个列表。
如你所见,我们常日会调用终端操作 collect 并给它通报一个网络器。
这里是很常见的实践——利用 collect 并将 Collectors.toList()通报给它,觉得就像是样板代码一样。
好是,在 Java 16 中 Stream API 中添加了一个新方法,使我们能够立即将 toList()作为一个流的一个终端操作来调用。
List<String> features = Stream.of("Records", "Pattern Matching", "Sealed Classes") .map(String::toLowerCase) .filter(s -> s.contains(" ")) .toList();
复制代码
在上面的代码中利用这个新方法会天生一个来自这个流,且包含一个空格的字符串的列表。请把稳,我们返回的这个列表是一个不可修正的列表。这意味着你不能再从这个终端操作返回的列表中添加或删除任何元素。如果要将流网络到一个可变列表中,则必须连续利用一个带有 collect()函数的网络器。以是这个 Java 16 中新引入的 toList()方法真的很讨喜。这个更新该当能让流管道代码块读起来更随意马虎一些。
Stream API 的另一个更新是 mapMulti()方法。它的用场有点像 flatMap()方法。如果你平常用的是 flatMap(),并且映射到 lambda 中的内部流并通报给它,那么 mapMulti()为你供应了一种替代方法,你可以将元素推送给一个消费者。我不会在本文中详细先容这个方法,由于我想谈论的是 Java 16 中的措辞新特性。如果你有兴趣进一步理解 mapMulti(),我强烈建议你查看Java文档中关于这种方法的先容。
RecordsJava 16 中引入的第一个重大措辞特性称为记录(records)。记录用来将数据表示为 Java 代码中的数据,而不是任意类。在 Java 16 之前,有时我们只是须要表示一些数据,终极却得到了一个任意类,如下面的代码段所示。
public class Product { private String name; private String vendor; Private int price; private boolean inStock;}
复制代码
这里我们有一个 Product 类,它有四个成员。定义这个类所需的所有信息该当便是这些了。当然,我们须要更多的代码来完成这项事情。例如,我们须要有一个布局器。我们须要有相应的 getter 方法来获取成员的值。为了补充完全,我们还须要有与我们定义的成员同等的 equals()、hashCode()和 toString()实现。一些样板代码可以由 IDE 天生,但这样做会有一些毛病。你也可以利用 Lombok 等框架,但它们也有一些毛病。
我们真正须要的是由 Java 措辞供应一种机制,可以更准确地描述拥有纯数据类这个观点。以是在 Java 16 中我们有了记录的观点。在以下代码段中,我们将 Product 类重新定义为一个记录。
public record Product( String name, String vendor, int price, boolean inStock) {}
复制代码
请把稳这里引入了新关键字 record。我们须要在关键字 record 之后指定记录类型的名称。在我们的示例中,这个名称是 Product。然后我们只须要供应组成这些记录的组件。在这里,我们给出了四个组件的类型和名称以供应它们。然后我们就完成了。Java 中的记录是类的一个分外形式,个中只包含数据。
记录能给我们带来什么呢?一旦我们有了一个记录声明,我们就会得到一个类,它有一个隐式布局器,接管这个记录的组件的所有值。我们会根据所有记录组件自动获取 equals()、hashCode()和 toString()方法的实现。此外,我们还为记录中的每个组件获取访问器方法。在上面的例子中,我们得到了一个 name 方法、一个 vendor 方法、一个 price 方法和一个 inStock 方法,它们分别返回这个记录的组件的实际值。
记录永久是不可变的。这里没有 setter 方法。一旦利用某些值实例化一个记录,那么你就无法再变动它了。此外,记录类便是终极形式。你可以利用一个记录实现一个接口,但在定义记录时不能扩展其他任何类。总而言之,这里有一些限定。但是记录为我们供应了一种非常强大的办法来在我们的运用程序中简洁地定义纯数据类。
若何看待记录你该当如何看待和处理这些新的措辞元素呢?记录是一种新的、受限形式的类,用于将数据建模为数据。我们不可能向记录添加任何附加状态;除了记录的组件之外,你不能定义(非静态)字段。记录实际上是建模不可变数据的。你也可以将记录视为元组,但它并不但是其他一些措辞所有的那种一样平常意义上的元组,在那种元组里有一些可以由索引引用的任意组件。在 Java 中,元组元素有实际名称,并且元组类型本身——即记录,也有一个名称,由于名称在 Java 中很主要。
记录不适宜哪些场景有些场景中我们可能会以为记录用起来并不是很得当。首先,它们并不是任何现有代码的一个样板缩减机制。虽然我们现在有一种非常简洁的办法来定义这些记录,但这并不虞味着你的运用程序中的任何数据(如类)都可以轻松地被记录更换,这紧张是由于记录存在的一些限定所致。这也不是它真正的设计目标。
记录的设计目标是供应一种将数据建模为数据的好方法。它也不是 JavaBeans 的直接替代品,由于正如我之条件到的,访问器这样的方法不符合 JavaBeans 的 get 标准。其余 JavaBeans 常日是可变的,而记录是不可变的。只管它们的用场有点像,但记录并不会以某种办法取代 JavaBean。你也不应该将记录视为值类型。
值类型可能会在未来的 Java 版本中作为措辞增强引入,其紧张关注内存布局和类中数据的有效表示。当然,这两条天下限在未来某一时候可能会合并在一起,但就目前而言,记录只是表达纯数据类的一种更简洁的办法。
进一步理解记录考虑以下代码,我们创建了 Product 类型的记录 p1 和 p2,具有完备相同的值。
Product p1 = new Product("peanut butter", "my-vendor", 20, true);Product p2 = new Product("peanut butter", "my-vendor", 20, true);
复制代码
我们可以通过引用相等来比较这些记录,也可以利用 equals()方法比较它们,该方法已由记录实现自动供应。
System.out.println(p1 == p2); // Prints falseSystem.out.println(p1.equals(p2)); // Prints true
复制代码
可以看到,这两条记录是两个不同的实例,因此引用比拟将给出 false。但是当我们利用 equals()时,它只查看这两个记录的值,以是它会评估为 true。由于它只考虑记录内部的数据。重申一下,相等性和哈希码的实现完备基于我们为记录的布局器供应的值。
须要把稳的一件事是,你仍旧可以覆盖记录定义中的任何访问器方法,或者相等性和哈希码实现。但是,你有任务在记录的高下文中保留这些方法的语义。并且你可以向记录定义添加其他方法。你还可以访问这些新方法中的记录值。
另一个你可能想在记录中实行的主要特性是验证。例如,你只想在供应给记录布局器的输入有效时才创建记录。传统的验证方法是定义一个带有输入参数的布局器,这些参数在将参数分配给成员变量之提高行验证。但是对付记录而言,我们可以利用一种新格式,即所谓的紧凑布局器。在这种格式中,我们可以省略正式的布局器参数。布局器将隐式地访问组件值。在我们的 Product 示例中,我们可以说如果 price 小于零,则抛出一个新的 IllegalArgumentException。
public record Product( String name, String vendor, int price, boolean inStock) { public Product { if (price < 0) { throw new IllegalArgumentException(); } }}
复制代码
从上面的代码段中可以看出,如果价格高于零,我们就不必手动做任何赋值。在编译此记录时,编译器会自动添加从(隐式)布局器参数到记录字段的赋值。
如果我们乐意,乃至可以进行正则化。例如,我们可以将隐式可用的价格参数设置为一个默认值,而不是在价格小于零时抛出非常。
public Product { if (price < 0) { price = 100; }}
复制代码
同样,对记录的实际成员的赋值——即作为这个记录定义一部分的终极字段,是由编译器在这个紧凑布局器的末端自动插入的。总而言之,这是在 Java 中定义纯数据类的一种非常通用且非常棒的方法。
你还可以在方法中本地声明和定义记录。如果你想在方法中利用一些中间状态,这会非常方便。例如,假设我们要定义一个打折产品。我们可以定义一个记录,包含 Product 和一个指示产品是否打折的 boolean 值。
public static void main(String... args) { Product p1 = new Product("peanut butter", "my-vendor", 100, true); record DiscountedProduct(Product product, boolean discounted) {} System.out.println(new DiscountedProduct(p1, true));}
复制代码
从上面的代码段中可以看出,我们不必为新记录定义供应正文。我们可以利用 p1 和 true 作为参数来实例化 DiscountedProduct。运行代码时,你会看到它的行为办法与源文件中的顶级记录完备相同。如果你希望在流管道的中间阶段分组某些数据,作为本地布局的记录会非常有用。
你会在哪里利用记录记录有一些显而易见的利用场景。比如说当我们想要利用数据传输工具(Data Transfer Objects,DTO)时就可以利用记录。根据定义,DTO 是不须要任何身份或行为的工具。它们只是用来传输数据的。例如,从 2.12 版本开始,Jackson 库支持将记录序列化和反序列化为 JSON 和其他支持的格式。
如果你希望一个映命中的键由充当复合键的多个值组成,记录也会很好用,由于你会自动得到 equals 和 hashcode 实现的精确行为。由于记录也可以被认为是名义元组(个中每个组件都有一个名称),利用记录将多个值从方法返回给调用者也是很方便的。
另一方面,我认为记录在 Java Persistence API 中用的不会很多。如果你想利用记录来表示实体,那实际上是不可能的,由于实体在很大程度上是基于 JavaBeans 约定。并且实体常日方向于是可变的。当然,当你在查询中实例化只读视图工具时,有些情形下你可以利用记录代替常规类。
总而言之,我认为 Java 中引入记录是一项激动民气的改进。我认为它们会得到广泛利用。
instanceof 的模式匹配Java 16 中的第二大措辞变动是 instanceof 的模式匹配。这是将模式匹配引入 Java 的漫长旅程的第一步。就目前而言,我认为 Java 16 中供应的初期支持已经很不错了。看看下面的代码段。
if (o instanceOf String) { String s = (String) o; return s.length();}
复制代码
你可能会认出这种模式,个中一些代码卖力检讨工具是否是一个类型的实例,在本例中是 String 类。如果检讨通过,我们须要声明一个新的浸染域变量,转换并赋值,然后我们才能开始利用这个类型化的变量。在这个示例中,我们须要声明变量 s,cast o 为一个 String,然后调用 length()方法。虽然这种办法也能用,但太啰嗦了,而且并没有反响出代码的真实意图。我们有更好的办法。
从 Java 16 开始,我们可以利用新的模式匹配特性了。利用模式匹配时,我们可以将 o 匹配一个类型模式,而不是说 o 是一个特定类型的实例。类型模式由一个类型和一个绑定变量组成。我们来看一个例子。
if (o instanceOf String s) { return s.length();}
复制代码
在上面的代码段中,如果 o 确实是 String 的实例,那么 String s 将立即绑定到 o 的值。这意味着我们可以立即开始利用 s 作为一个字符串,而无需在 if 主体内进行显式转换。这里的另一个好处是 s 的浸染域仅限于 if 的主体。这里须要把稳的一点是,源代码中 o 的类型不应该是 String 的子类型,由于如果是这种情形,条件将始终为真。因此一样平常而言,如果编译器检测到正在测试的工具的类型是模式类型的子类型,则会抛出编译时缺点。
另一个须要指出的有趣的事情是,编译器很聪明,可以根据条件的打算结果为 true 还是 false 来推断 s 的浸染域,正如以下代码段中所示。
if (!(o instanceOf String s)) { return 0;} else { return s.length();}
复制代码
编译器看到,如果模式匹配不堪利,那么在 else 分支中,我们的 s 将在 String 类型的浸染域内。并且在 if 分支 s 不在浸染域内时,我们在浸染域内就只有 o。这种机制称为流浸染域,个中类型模式变量仅在模式实际匹配时才在浸染域内。这真的很方便,能够有效简化这段代码。你须要把稳这个变革,可能须要一点韶光来适应。
另一个例子里你也可以清楚地看到这个流的浸染。当你重写 equals()方法的以下代码实现时,常规的实现是首先检讨 o 是否是 MyClass 的一个实例。如果是,我们将 o 转换为 MyClass,然后将 o 的 name 字段与 MyClass 确当前实例进行匹配。
@Overridepublic boolean equals(Object o) { return (o instanceOf MyClass) && ((MyClass) o).name.equals(name);}
复制代码
我们可以利用新的模式匹配机制来简化这个实现,如下面的代码段所示。
@Overridepublic boolean equals(Object o) { return (o instanceOf MyClass m) && m.name.equals(name);}
复制代码
这里又一次对代码中显式、冗长的转换做了很好的简化。只要用在得当的用例里,模式匹配会抽象出许多样板代码。
模式匹配:未来发展Java 团队已经勾勒出了模式匹配的一些未来发展方向。当然,团队并没有承诺这些设想何时或如何引入官方措辞。不才面的代码段中可以看到,在新的 switch 表达式中,我们可以像之前谈论的那样利用 instanceOf 来做类型模式。
static String format(Object o) { return switch(o) { case Integer i -> String.format("int %d", i); case Double d -> String.format("int %f", d); default -> o.toString(); };}
复制代码
在 o 是整数的情形下,流浸染域开始起浸染,我们可以立即将变量 i 用作一个整数。其他情形和默认分支也是如此。
另一个令人愉快的新方向是记录模式,我们可以模式匹配我们的记录并立即将组件值绑定到新变量。看看下面的代码段。
if (o instanceOf Point(int x, int y)) { System.out.println(x + y);}
复制代码
我们有一个包含 x 和 y 的 Point 记录。如果工具 o 确实是一个点,我们将立即将 x 和 y 分量绑定到 x 和 y 变量并立即开始利用它们。
数组模式是可能在 Java 的未来版本中引入的另一种模式匹配。看看下面的代码段。
if (o instanceOf String[] {String s1, String s2, ...}) { System.out.println(s1 + s2);}
复制代码
如果 o 是字符串数组,则可以立即将这个字符串数组的第一部分和第二部分提取到 s1 和 s2。当然,这只适用于字符串数组中有两个或更多元素的情形。我们可以利用三点表示法忽略数组元素的别的部分。
总而言之,利用 instanceOf 进行模式匹配是一个不错的小特性,但它是迈向新未来的一步。我们可能会引入其他类型的模式来帮助编写干净、大略和可读的代码。
特性预览:密封类下面来谈谈密封类(sealed class)这个特性。请把稳,这是 Java 16 中的预览特性,将在 Java 17 中成为终极版本。你须要将--enable-preview 标志通报给编译器调用和 JVM 调用才能在 Java 16 中利用这个特性。该特性许可你掌握继续层次构造。
假设你想对一个超类型 Option 建模,个中你只想有 Some 和 Empty 两个子类型。并且你想预防 Option 类型得到任何扩展。例如,你不想在层次构造中许可 Maybe 类型。
因此,你已经详细描述了 Option 类型的所有子类型。如你所知,目前在 Java 中掌握继续的唯一工具是通过 final 关键字。这意味着根本不能有任何子类,但这不是我们想要的。有一些办理方法可以在没有密封类的情形下建模这个特性,但有了密封类后就随意马虎多了。
密封类特性带有新的关键字 sealed 和 permits。看看下面的代码段。
public sealed class Option<T> permits Some, Empty { ...}public final class Some extends Option<String> { ...}public final class Empty extends Option<Void> { ...}
复制代码
我们可以定义要 sealed 的 Option 类。然后,在类声明之后,我们利用 permit 关键字来规定只许可 Some 和 Empty 类扩展 Option 类。然后,我们可以像往常一样将 Some 和 Empty 定义为类。我们希望将这些子类设为 final,以防止进一步继续。现在系统就不能编译其他类来扩展 Option 类了,这是由编译器通过密封类机制逼迫实行的。
关于此特性还有很多要说的内容,本文不能逐一尽述。如果你有兴趣理解更多信息,我建议你浏览密封类Java增强提案页面JEP360。
小结Java 16 中还有很多我们无法在本文中先容的内容,例如Vector API、Foreign Linker API和Foreign-Memory Access API等孵化器 API 都非常有出息。并且新版在 JVM 层面也做了很多改进。例如ZGC有一些性能改进;在 JVM 中做了一些Elastic Metaspace改进;还有一个新的 Java 运用程序打包工具,许可你为 Windows、Mac 和 Linux 创建原生安装程序。末了,当你从 classpath 运行运用程序时,JDK 中的封装类型将受到严格保护,我认为这也会有很大影响。
我强烈建议你研究所有这些新特性和措辞增强,由于个中一些改进会对你的运用程序产生重大影响。
作者先容Sander Mak是一名 Java Champion,在 Java 社区生动了十多年。目前他是 Picnic 的技能总监。同时,Mal 也常常做知识分享,通过各种会媾和在线电子学习平台传授履历。
原文链接:
What's New in Java 16