写点什么

从头学 Java17-Stream API(二)结合 Record、Optional

作者:烧霞
  • 2023-07-05
    广西
  • 本文字数:29005 字

    阅读完需:约 95 分钟

Stream API

Stream API 是按照 map/filter/reduce 方法处理内存中数据的最佳工具。本系列教程由 Record 讲起,然后结合 Optional,讨论 collector 的设计。


使用 Record 对不可变数据进行建模

Java 语言为您提供了几种创建不可变类的方法。可能最直接的是创建一个包含 final 字段的 final 类。下面是此类的示例。


public final class Point {    private final int x;    private final int y;
public Point(int x, int y) { this.x = x; this.y = y; }}
复制代码


编写这些元素后,需要为字段添加访问器。您还将添加一个 toString() 方法,可能还有一个 equals() 以及一个 hashCode() 方法。手写所有这些非常乏味且容易出错,幸运的是,您的 IDE 可以为您生成这些方法。


如果需要通过网络或文件系统将此类的实例从一个应用程序传送到另一个应用程序,则还可以考虑使此类可序列化。如果这样做,还要添加一些有关如何序列化的信息。JDK 为您提供了几种控制序列化的方法。


最后,您的Point类可能有一百多行,主要是 IDE 生成的代码,只是为了对需要写入文件的两个整数不可变集进行建模。


Record 已经添加到 JDK 以改变这一切。只需一行代码即可为您提供所有这些。您需要做的就是声明 record 的状态;其余部分由编译器为您生成。

呼叫 Record 支援

Record 可帮助您使此代码更简单。从 Java SE 14 开始,您可以编写以下代码。


public record Point(int x, int y) {}
复制代码


这一行代码为您创建以下元素。


  1. 它是一个不可变的类,有两个字段:xy

  2. 它有一个标准的构造函数,用于初始化这两个字段。

  3. toString()、equals() 和 hashCode() 方法是由编译器为您创建的,其默认行为与 IDE 将生成的内容相对应。如果需要,可以通过添加自己的实现来修改此行为。

  4. 它可以实现Serializable接口,以便您可以通过网络或通过文件系统发送到其他应用程序。序列化和反序列化 record 的方式遵循本教程末尾介绍的一些特殊规则。


record 使创建不可变的数据集变得更加简单,无需任何 IDE 的帮助。降低了错误的风险,因为每次修改 record 的组件时,编译器都会自动更新 equals() 和 hashCode() 方法。

record 的类

record 也是类,是用关键字record而不是class声明的类。让我们声明以下 record。


public record Point(int x, int y) {}
复制代码


编译器在创建 record 时为您创建的类是 final 的。


此类继承了 java.lang.Record 类。因此,您的 record 不能继承其他任何类。


一条 record 可以实现任意数量的接口。

声明 record 的组成部分

紧跟 record 名称的块是(int x, int y) 。它声明了 record 组件。对于 record 的每个组件,编译器都会创建一个同名的私有 final 字段。您可以在 record 中声明任意数量的组件。


除了字段,编译器还为每个组件生成一个访问器。此访问器跟组件的名称相同,并返回其值。对于此 record,生成的两个方法如下。


public int x() {    return this.x;}
public int y() { return this.y;}
复制代码


如果此实现适用于您的应用程序,则无需添加任何内容。不过,也可以定义自己的访问器。


编译器为您生成的最后一个元素是 Object 类中 toString()、equals() 和 hashCode() 方法的重写。如果需要,您可以定义自己对这些方法的覆盖。

无法添加到 record 的内容

有三件事不能添加到 record 中:


  1. 额外声明的实例字段。不能添加任何与组件不对应的实例字段。

  2. 实例字段的初始化。

  3. 实例的初始化块。


您可以使用静态字段,静态初始化块。

使用标准构造函数构造 record

编译器还会为您创建一个构造函数,称为标准构造函数 canonical constructor。此构造函数以 record 的组件作为参数,并将其值复制到字段中。


在某些情况下,您需要覆盖此默认行为。让我们研究两种情况:


  1. 您需要验证组件的状态

  2. 您需要制作可变组件的副本。

使用紧凑构造函数

可以使用两种不同的语法来重新定义 record 的标准构造函数。可以使用紧凑构造函数或标准构造函数本身。


假设您有以下 record。


public record Range(int start, int end) {}
复制代码


对于该名称的 record,应该预期 end大于start .您可以通过在 record 中编写紧凑构造函数来添加验证规则。


public record Range(int start, int end) {
public Range {//不需要参数块 if (end <= start) { throw new IllegalArgumentException("End cannot be lesser than start"); } }}
复制代码


紧凑构造函数不需要声明其参数块。


请注意,如果选择此语法,则无法直接分配 record 的字段,例如this.start = start - 这是通过编译器添加代码为您完成的。 但是,您可以为参数分配新值,这会导致相同的结果,因为编译器生成的代码随后会将这些新值分配给字段。


public Range {    // set negative start and end to 0    // by reassigning the compact constructor's    // implicit parameters    if (start < 0)        start = 0;//无法给this.start赋值    if (end < 0)        end = 0;}
复制代码

使用标准构造函数

如果您更喜欢非紧凑形式(例如,因为您不想重新分配参数),则可以自己定义标准构造函数,如以下示例所示。


public record Range(int start, int end) {//跟紧凑构造不能共存
public Range(int start, int end) { if (end <= start) { throw new IllegalArgumentException("End cannot be lesser than start"); } if (start < 0) { this.start = 0; } else { this.start = start; } if (end > 100) { this.end = 10; } else { this.end = end; } }}
复制代码


这种情况下,您编写的构造函数需要为 record 的字段手动赋值。


如果 record 的组件是可变的,则应考虑在标准构造函数和访问器中制作它们的副本。

自定义构造函数

还可以向 record 添加自定义构造函数,只要此构造函数内调用 record 的标准构造函数即可。语法与经典语法相同。对于任何类,调用this()必须是构造函数的第一个语句。


让我们检查以下Staterecord。它由三个组件定义:


  1. 此州的名称

  2. 该州首府的名称

  3. 城市名称列表,可能为空。


我们需要存储城市列表的副本,确保它不会从此 record 的外部修改。 这可以通过使用紧凑形式,将参数重新分配给副本。


拥有一个不用城市作参数的构造函数在您的应用程序中很有用。这可以是另一个构造函数,它只接收州名和首都名。第二个构造函数必须调用标准构造函数。


然后,您可以将城市作为 vararg 传递。为此,您可以创建第三个构造函数。


public record State(String name, String capitalCity, List<String> cities) {
public State { // List.copyOf returns an unmodifiable copy, // so the list assigned to `cities` can't change anymore cities = List.copyOf(cities); }
public State(String name, String capitalCity) { this(name, capitalCity, List.of()); }
public State(String name, String capitalCity, String... cities) { this(name, capitalCity, List.of(cities));//也是不可变的 }
}
复制代码


请注意,List.copyOf() 方法的参数不接受空值。

获取 record 的状态

您不需要向 record 添加任何访问器,因为编译器会为您执行此操作。一条 record 的每个组件都有一个访问器方法,该方法具有此组件的名称。


但是,某些情况下,您需要定义自己的访问器。 例如,假设上一节中的Staterecord 在构造期间没有创建列表的不可修改的副本 - 那么它应该在访问器中执行此操作,以确保调用方无法改变其内部状态。 您可以在 record 中添加以下代码以返回此副本。


public List<String> cities() {    return List.copyOf(cities);}
复制代码

序列化 record

如果您的 record 类实现了可序列化,则可以序列化和反序列化record。不过也有限制。


  1. 可用于替换默认序列化过程的任何系统都不适用于 record。创建 writeObject() 和 readObject() 方法不起作用,也不能实现 Externalizable

  2. record 可用作代理对象来序列化其他对象。readResolve() 方法可以返回 record。也可以在 record 中添加 writeReplace()。

  3. 反序列化 record 始终调用标准构造函数。因此,在此构造函数中添加的所有验证规则都将在反序列化 record 时强制执行。


这使得 record 在应用程序中作为数据传输对象非常合适。

在实际场景中使用 record

record 是一个多功能的概念,您可以在许多上下文中使用。


第一种方法是在应用程序的对象模型中携带数据。用 record 充当不可变的数据载体,也是它们的设计目的。


由于可以声明本地 record,因此还可以使用它们来提高代码的可读性。


让我们考虑以下场景。您有两个建模为 record 的实体:CityState


public record City(String name, State state) {}
复制代码


public record State(String name) {}
复制代码


假设您有一个城市列表,您需要计算拥有最多城市数量的州。可以使用 Stream API 首先使用每个州拥有的城市数构建各州的柱状图。此柱状图由Map建模。


List<City> cities = List.of();
Map<State, Long> numberOfCitiesPerState = cities.stream() .collect(Collectors.groupingBy( City::state, Collectors.counting() ));
复制代码


获取此柱状图的最大值是以下代码。


Map.Entry<State, Long> stateWithTheMostCities =    numberOfCitiesPerState.entrySet().stream()                          .max(Map.Entry.comparingByValue())//最多城市                          .orElseThrow();
复制代码


最后一段代码是技术性的;它不具有任何业务意义;因为使用Map.Entry实例对柱状图的每个元素进行建模。


使用本地 record 可以大大改善这种情况。下面的代码创建一个新的 record 类,该类包含一个州和该州的城市数。它有一个构造函数,该构造函数将 Map.Entry 的实例作为参数,将键值对流映射到 record 流。


由于需要按城市数比较这些集,因此可以添加工厂方法来提供此比较器。代码将变为以下内容。


record NumberOfCitiesPerState(State state, long numberOfCities) {
public NumberOfCitiesPerState(Map.Entry<State, Long> entry) { this(entry.getKey(), entry.getValue());//mapping过程 }
public static Comparator<NumberOfCitiesPerState> comparingByNumberOfCities() { return Comparator.comparing(NumberOfCitiesPerState::numberOfCities); }}
NumberOfCitiesPerState stateWithTheMostCities = numberOfCitiesPerState.entrySet().stream() .map(NumberOfCitiesPerState::new) .max(NumberOfCitiesPerState.comparingByNumberOfCities())//record替换Entry .orElseThrow();
复制代码


您的代码现在以有意义的方式提取最大值。您的代码更具可读性,更易于理解,不易出错,从长远来看更易于维护。

使用 collector 作为末端操作

让我们回到 Stream API。

使用 collector 收集流元素

您已经使用了一个非常有用的模式collect(Collectors.toList())来收集由 List 中的流处理的元素。此 collect() 方法是在 Stream 接口中定义的末端方法,它将 Collector 类型的对象作为参数。此Collector接口定义了自己的 API,可用于创建任何类型的内存中结构来存储流处理的数据。可以在CollectionMap的任何实例中进行收集,它可用来创建字符串,并且您可以创建自己的Collector实例以将自己的结构添加到列表中。


将使用的大多数 collector 都可以使用 Collectors 工厂类的工厂方法之一创建。这是您在编写 Collectors.toList() 或 Collectors.toSet() 时所做的。使用这些方法创建的一些 collector 可以组合使用,从而产生更多的 collector。本教程涵盖了所有这些要点。


如果在此工厂类中找不到所需的内容,则可以决定通过实现 Collector 接口来创建自己的 collector。本教程还介绍了如何实现此接口。


Collector API 在 Stream 接口和专用数字流IntStreamLongStreamDoubleStream中的处理方式不同:。Stream 接口有两个 collect() 方法重载,而数字流只有一个。缺少的正是将 collector 对象作为参数的那个。因此,不能将 collector 对象与专用的数字流一起使用。

在集合中收集

Collectors工厂类提供了三种方法,用于在Collection接口的实例中收集流的元素。


  1. toList() 将它们收集在 List 对象中。

  2. toSet() 将它们收集在 Set 对象中。

  3. 如果需要任何其他Collection实现,可以使用 toCollection(supplier),其中 supplier 参数将用于创建所需的 Collection 对象。如果您需要在 LinkedList 实例中收集您的数据,您应该使用此方法。


代码不应依赖于这些方法当前返回的 ListSet 的确切实现,因为它不是标准的一部分。


您还可以使用 unmodifiableList()toUnmodifiableSet() 两种方法获取 ListSet 的不可变实现。


以下示例显示了此模式的实际应用。首先,让我们在一个普通List实例中收集。


List<Integer> numbers =IntStream.range(0, 10)         .boxed()//需要装箱         .collect(Collectors.toList());System.out.println("numbers = " + numbers);
复制代码


此代码使用 boxed() 中继方法从 IntStream.range() 创建的 IntStream 创建一个 Stream,方法是对该流的所有元素进行装箱。运行此代码将打印以下内容。


numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
复制代码


第二个示例创建一个只有偶数且没有重复项的 HashSet


Set<Integer> evenNumbers =IntStream.range(0, 10)         .map(number -> number / 2)         .boxed()        .collect(Collectors.toSet());System.out.println("evenNumbers = " + evenNumbers);
复制代码


运行此代码将产生以下结果。


evenNumbers = [0, 1, 2, 3, 4]
复制代码


最后一个示例使用 Supplier 对象来创建用于收集流元素的 LinkedList 实例。


LinkedList<Integer> linkedList =IntStream.range(0, 10)         .boxed()         .collect(Collectors.toCollection(LinkedList::new));System.out.println("linked listS = " + linkedList);
复制代码


运行此代码将产生以下结果。


linked list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
复制代码

使用 collector 计数

Collectors 工厂类为您提供了几种方法来创建 collector,这些 collector 执行的操作与普通末端方法为您提供的操作相同。Collectors.counting() 工厂方法就是这种情况,它与在流上调用 count() 相同。


这是值得注意的,您可能想知道为什么使用两种不同的模式实现了两次这样的功能。将在下一节有关在 map 中收集时回答此问题,您将在其中组合 collector 以创建更多 collector。


目前,编写以下两行代码会导致相同的结果。


Collection<String> strings = List.of("one", "two", "three");
long count = strings.stream().count();long countWithACollector = strings.stream().collect(Collectors.counting());
System.out.println("count = " + count);System.out.println("countWithACollector = " + countWithACollector);
复制代码


运行此代码将产生以下结果。


count = 3countWithACollector = 3
复制代码

收集在字符串中

Collectors 工厂类提供的另一个非常有用的 collector 是 joining() 。此 collector 仅适用于字符串流,并将该流的元素连接为单个字符串。它有几个重载。


  • 第一个将分隔符作为参数。

  • 第二个将分隔符、前缀和后缀作为参数。


让我们看看这个 collector 的实际效果。


String joined =     IntStream.range(0, 10)             .boxed()             .map(Object::toString)             .collect(Collectors.joining());
System.out.println("joined = " + joined);
复制代码


运行此代码将生成以下结果。


joined = 0123456789
复制代码


可以使用以下代码向此字符串添加分隔符。


String joined =     IntStream.range(0, 10)             .boxed()             .map(Object::toString)             .collect(Collectors.joining(", "));
System.out.println("joined = " + joined);
复制代码


结果如下。


joined = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
复制代码


让我们看看最后一个重载,它接收分隔符、前缀和后缀。


String joined =     IntStream.range(0, 10)             .boxed()             .map(Object::toString)             .collect(Collectors.joining(", ", "{"), "}");
System.out.println("joined = " + joined);
复制代码


结果如下。


joined = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
复制代码


请注意,此 collector 可以正确处理流为空或仅处理单个元素的极端情况。


当您需要生成此类字符串时,此 collector 非常方便。即使您前面的数据不在集合中或只有几个元素,您也可能想使用它。如果是这种情况,使用 String.join() 工厂类或 StringJoiner 对象都将正常工作,无需支付创建流的开销。

使用 Predicate 对元素进行分区

Collector API 提供了三种模式,用于从流的元素创建 map。我们介绍的第一个使用布尔键创建 map。它是使用 partitionningBy() 工厂方法创建的。


流的所有元素都将绑定到布尔值truefalse。map 将绑定到每个值的所有元素存储在列表中。因此,如果将此 collector 应用于Stream,它将生成具有以下类型的 map:Map<Boolean,List<T>>


测试的 Predicate 应作为参数提供给 collector。


下面的示例演示此 collector 的操作。


Collection<String> strings =    List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine",            "ten", "eleven", "twelve");
Map<Boolean, List<String>> map = strings.stream() .collect(Collectors.partitioningBy(s -> s.length() > 4));
map.forEach((key, value) -> System.out.println(key + " :: " + value));
复制代码


运行此代码将生成以下结果。


false :: [one, two, four, five, six, nine, ten]true :: [three, seven, eight, eleven, twelve]
复制代码


此工厂方法具有重载,它将另一个 collector 作为参数。此 collector 称为下游 collector。我们将在本教程的下一段中介绍,届时我们将介绍 groupingBy()

在 map 中收集并进行分组

我们提供的第二个 collector 非常重要,因为它允许您创建柱状图。

对 map 中的流元素进行分组

可用于创建柱状图的 collector 是使用 Collectors.groupingBy() 方法创建的。此方法具有多个重载。


collector 将创建 map。通过对其应用 Function 实例,为流的每个元素计算一个键。此函数作为 groupingBy() 方法的参数提供。它在 Collector API 中称为分类器 classifier


除了不应该返回 null 之外,此函数没有任何限制。


此函数可能会为流的多个元素返回相同的键。groupingBy() 支持这一点,并将所有这些元素收集在一个列表中。


因此,如果您正在处理 Stream 并使用 Function<T, K> 作为分类器,则 groupingBy() 会创建一个 Map<K,List<T>>


让我们检查以下示例。


Collection<String> strings =    List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine",            "ten", "eleven", "twelve");
Map<Integer, List<String>> map = strings.stream() .collect(Collectors.groupingBy(String::length));//返回<Integer, List<String>>
map.forEach((key, value) -> System.out.println(key + " :: " + value));
复制代码


此示例中使用的分类器是一个函数,用于从该流返回每个字符串的长度。因此,map 按字符串长度将字符串分组到列表中。它具有Map<Interger,List<String>>的类型。


运行此代码将打印以下内容。


3 :: [one, two, six, ten]4 :: [four, five, nine]5 :: [three, seven, eight]6 :: [eleven, twelve]
复制代码

对分组后的值进行处理

计算数量

groupingBy() 方法还接受另一个参数,即另一个 collector。此 collector 在 Collector API 中称为下游 collector,但它没有什么特别的。使它成为下游 collector 的原因只是,它作为参数传递给前一个 collector 的创建。


此下游 collector 用于收集由 groupingBy() 创建的 map 的值。


在前面的示例中,groupingBy() 创建了一个 map,其值是字符串列表。如果为 groupingBy() 方法提供下游 collector,API 将逐个流式传输这些列表,并使用下游 collector 收集这些流。


假设您将 Collectors.counting() 作为下游 collector 传递。将计算的内容如下。


[one, two, six, ten]  .stream().collect(Collectors.counting()) -> 4L[four, five, nine]    .stream().collect(Collectors.counting()) -> 3L[three, seven, eight] .stream().collect(Collectors.counting()) -> 3L[eleven, twelve]      .stream().collect(Collectors.counting()) -> 2L
复制代码


此代码不是 Java 代码,因此您无法执行它。它只是在那里解释如何使用这个下游 collector。


下面将创建的 map 取决于您提供的下游 collector。键不会修改,但值可能会。在 Collectors.counting() 的情况下,值将转换为 Long。然后,map 的类型将变为 Map<Integer,Long>


前面的示例变为以下内容。


Collection<String> strings =        List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine",                "ten", "eleven", "twelve");
Map<Integer, Long> map = strings.stream() .collect( Collectors.groupingBy( String::length, Collectors.counting()));//List<String>转为Stream向下传递,变成Long
map.forEach((key, value) -> System.out.println(key + " :: " + value));
复制代码


运行此代码将打印以下结果。它给出了每个长度的字符串数,这是字符串长度的柱状图。


3 :: 44 :: 35 :: 36 :: 2
复制代码

连接列表的值

您还可以将 Collectors.joining() collector 作为下游 collector 传递,因为此 map 的值是字符串列表。请记住,此 collector 只能用于字符串流。这将创建 Map<Integer,String> 的实例。您可以将上一个示例更改为以下内容。


Collection<String> strings =        List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine",                "ten", "eleven", "twelve");
Map<Integer, String> map = strings.stream() .collect( Collectors.groupingBy( String::length, Collectors.joining(", ")));//变成Stringmap.forEach((key, value) -> System.out.println(key + " :: " + value));
复制代码


运行此代码将生成以下结果。


3 :: one, two, six, ten4 :: four, five, nine5 :: three, seven, eight6 :: eleven, twelve
复制代码

控制 map 的实例

groupingBy() 方法的最后一个重载将supplier的实例作为参数,以便您控制需要此 collector 创建的 Map 实例。


您的代码不应依赖于 groupingBy() 返回的确切 map 类型,因为它不是标准的一部分。

使用 ToMap 在 map 中收集

Collector API 为您提供了创建 map 的第二种模式:Collectors.toMap() 模式。此模式适用于两个函数,这两个函数都应用于流的元素。


  1. 第一个称为密钥 mapper,用于创建密钥。

  2. 第二个称为值 mapper,用于创建值。


此 collector 的使用场景与 Collectors.groupingBy() 不同。特别是,它不处理流的多个元素生成相同密钥的情况。这种情况下,默认情况下会引发IllegalStateException


这个 collector 能非常方便的创建缓存。假设User类有一个类型为 LongprimaryKey属性。您可以使用以下代码创建User对象的缓存。


List<User> users = ...;
Map<Long, User> userCache = users.stream() .collect(User::getPrimaryKey, Function.idendity());//key必须不同
复制代码


使用 Function.identity() 工厂方法只是告诉 collector 不要转换流的元素。


如果您希望流的多个元素生成相同的键,则可以将进一步的参数传递给 toMap() 方法。此参数的类型为 BinaryOperator。当检测到冲突元素时,实现将它应用于冲突元素。然后,您的 binary operator 将生成一个结果,该结果将代替先前的值放入 map 中。


下面演示如何使用具有冲突值的此 collector。此处的值用分隔符连接在一起。


Collection<String> strings =    List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine",            "ten", "eleven", "twelve");
Map<Integer, String> map = strings.stream() .collect( Collectors.toMap( element -> element.length(), element -> element, (element1, element2) -> element1 + ", " + element2));//相同key,解决冲突,返回新值
map.forEach((key, value) -> System.out.println(key + " :: " + value));
复制代码


在此示例中,传递给 toMap() 方法的三个参数如下:


  1. element -> element.length()键 mapper

  2. element -> element值 mapper

  3. (element1, element2) -> element1 + ", " + element2)合并函数,相同键的两个元素会调用。


运行此代码将生成以下结果。


3 :: one, two, six, ten4 :: four, five, nine5 :: three, seven, eight6 :: eleven, twelve
复制代码


另外也可以将 supplier 作为参数传递给 toMap() 方法,以控制此 collector 将使用的 Map 接口实例。


toMap() collector 有一个孪生方法 toConcurrentMap(),它将在并发 map 中收集数据。实现不保证 map 的确切类型。

从柱状图中提取最大值

groupingBy() 是分析计算柱状图的最佳模式。让我们研究一个完整的示例,其中您构建柱状图,然后尝试根据要求找到其中的最大值。

提取唯一的最大值

您要分析的柱状图如下。它看起来像我们在前面的示例中使用的那个。


Collection<String> strings =    List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine",            "ten", "eleven", "twelve");
Map<Integer, Long> histogram = strings.stream() .collect( Collectors.groupingBy( String::length, Collectors.counting()));
histogram.forEach((key, value) -> System.out.println(key + " :: " + value));
复制代码


打印此柱状图将得到以下结果。


3 :: 4 //期望是4 =>34 :: 35 :: 36 :: 2
复制代码


从此柱状图中提取最大值应得到结果:3 :: 4。Stream API 具有提取最大值所需的所有工具。不幸的是,Map接口上没有stream()方法。要在 map 上创建流,您首先需要获取可以从 map 获取的集合之一。


  1. entrySet() 方法的映射集。

  2. keySet() 方法的键集。

  3. 或者使用 values() 方法收集值。


这里你需要键和最大值,所以正确的选择是流式传输 entrySet() 返回的集合。


您需要的代码如下。


Map.Entry<Integer, Long> maxValue =    histogram.entrySet().stream()             .max(Map.Entry.comparingByValue())             .orElseThrow();
System.out.println("maxValue = " + maxValue);
复制代码


您可以注意到,此代码使用 Stream 接口中的 max() 方法,该方法将 comparator 作为参数。实际上,Map.Entry 接口的确有几个工厂方法来创建这样的 comparator。我们在此示例中使用的这个,创建了一个可以比较 Map.Entry 实例的 comparator,使用这些键值对的值。仅当值实现Comparable接口时,此比较才有效。


这种代码模式非常普通,只要具有可比较的值,就可以在任何 map 上使用。我们可以使其特别一点,更具可读性,这要归功于 Java SE 16 中记录 Record 的引入。


让我们创建一个 record 来模拟此 map 的键值对。创建 record 只需要一行。由于该语言允许 local records,因此您可以到任何方法中。


record NumberOfLength(int length, long number) {        static NumberOfLength fromEntry(Map.Entry<Integer, Long> entry) {        return new NumberOfLength(entry.getKey(), entry.getValue());//mapping过程    }
static Comparator<NumberOfLength> comparingByLength() { return Comparator.comparing(NumberOfLength::length); }}
复制代码


使用此 record,以前的模式将变为以下内容。


NumberOfLength maxNumberOfLength =    histogram.entrySet().stream()             .map(NumberOfLength::fromEntry)             .max(NumberOfLength.comparingByLength())//Record替换Entry,后面要引用字段             .orElseThrow();
System.out.println("maxNumberOfLength = " + maxNumberOfLength);
复制代码


运行此示例将打印出以下内容。


maxNumberOfLength = NumberOfLength[length=3, number=4]
复制代码


您可以看到此 record 看起来像 Map.Entry 接口。它有一个 mapping 键值对的工厂方法和一个用于创建 comparator 的工厂方法。柱状图的分析变得更加可读和易于理解。

提取多个最大值

前面的示例是一个很好的示例,因为列表中只有一个最大值。不幸的是,现实生活中的情况通常不是那么好,您可能有几个与最大值匹配的键值对。


让我们从上一个示例的集合中删除一个元素。


Collection<String> strings =    List.of("two", "three", "four", "five", "six", "seven", "eight", "nine",            "ten", "eleven", "twelve");
Map<Integer, Long> histogram = strings.stream() .collect( Collectors.groupingBy( String::length, Collectors.counting()));
histogram.forEach((key, value) -> System.out.println(key + " :: " + value));
复制代码


打印此柱状图将得到以下结果。


3 :: 34 :: 35 :: 3//期望是3 =>[3,4,5]6 :: 2
复制代码


现在我们有三个键值对的最大值。如果使用前面的代码模式提取它,则将选择并返回这三个中的一个,隐藏其他两个。


解决此问题的解决方案是创建另一个 map,其中键是字符串数量,值是与之匹配的长度。换句话说:您需要反转此 map。对于 groupingBy() 来说,这是一个很好的场景。此示例将在本部分的后面介绍,因为我们还需要一个元素来编写此代码。

使用中继 collector

到目前为止,我们介绍的 collector 只是计数、连接和收集到列表或 map 中。它们都属于末端操作。Collector API 也提供了执行中继操作的其他 collector:mapping、filtering 和 flatmapping。您可能想知道这样的意义是什么。事实上,这些特殊的 collector 并不能单独创建。它们的工厂方法都需要下游 collector 作为第二个参数。


也就是说,您这样创建的整体 collector 是中继操作和末端操作的组合。

使用 collector 来 mapping

我们可以检查的第一个中继操作是 mapping 操作。mapping collector 是使用 Collectors.mapping() 工厂方法创建的。它将常规 mapping 函数作为第一个参数,将必需的下游 collector 作为第二个参数。


在下面的示例中,我们将 mapping 与列表中 mapping 后的元素的集合相结合。


Collection<String> strings =    List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine",            "ten", "eleven", "twelve");
List<String> result = strings.stream() .collect( Collectors.mapping(String::toUpperCase, Collectors.toList()));//集成了mapping
System.out.println("result = " + result);
复制代码


Collectors.mappping() 工厂方法创建一个常规 collector。您可以将此 collector 作为下游 collector 传递给任何接受 collector 的方法,例如,包括 groupingBy()toMap()。您可能还记得在“提取多个最大值”一节中,我们留下了一个关于反转 map 的悬而未决的问题。让我们使用这个 mapping collector 来解决问题。


在此示例中,您创建了一个柱状图。现在,您需要使用 groupingBy() 反转此柱状图以查找所有最大值。


以下代码创建此类 map。


Map<Integer, Long> histogram = ...; 
var map = histogram.entrySet().stream() .map(NumberOfLength::fromEntry) .collect( Collectors.groupingBy(NumberOfLength::number));//<Long, List<NumberOfLength>>
复制代码


让我们检查此代码并确定所构建 map 的确切类型。


此 map 的键是每个长度在原始流中存在的次数。它是NumberOfLengthrecord 的number部分,Long。类型。


这些值是此流的元素,收集到列表中。因此,是NumberOfLength的对象列表。这张 map 的确切类型是Map<Long,NumberOfLength>


当然,这不是您所要的。您需要的只是字符串的长度,而不是 record。从 record 中提取组件是一个 mapping 过程。您需要将这些NumberOfLength实例 mapping 为其length组件。现在我们介绍了 mapping collector,可以解决这一点。您需要做的就是将正确的下游 collector 添加到 groupingBy() 调用中。


代码将变为以下内容。


Map<Integer, Long> histogram = ...; 
var map = histogram.entrySet().stream() .map(NumberOfLength::fromEntry) .collect( Collectors.groupingBy( NumberOfLength::number, Collectors.mapping(NumberOfLength::length, Collectors.toList())));//<Long, List<Integer>>
复制代码


构建的 map 的值现在是使用NumberOfLength::lengthNumberOfLength做 mapping 后生成的对象列表。此 map 的类型为Map<Long,List<Integer>>,这正是您所需要的。


要获取所有最大值,您可以像之前那样,使用 key 获取最大值而不是值。


柱状图中的完整代码,包括最大值提取,如下所示。


Map<Long, List<Integer>> map =    histogram.entrySet().stream()             .map(NumberOfLength::fromEntry)             .collect(                Collectors.groupingBy(                    NumberOfLength::number,//变成了number=>length列表                    Collectors.mapping(NumberOfLength::length, Collectors.toList())));
Map.Entry<Long, List<Integer>> result = map.entrySet().stream() .max(Map.Entry.comparingByKey())//再求key的max .orElseThrow();
System.out.println("result = " + result);
复制代码


运行此代码将生成以下内容。


result = 3=[3, 4, 5]//最多的length列表
复制代码


这意味着有三种长度的字符串在此流中出现三次:3、4 和 5。


此示例显示嵌套在另外两个 collector 中的 collector,在使用此 API 时,这种情况经常发生。乍一看可能看起来很吓人,但它只是使用下游 collector 组合成了 collector。


您可以看到为什么拥有这些中继 collector 很有趣。通过使用 collector 提供的中继操作,您可以为几乎任何类型的处理创建下游 collector,从而对 map 的值进行后续处理。

使用 collector 进行 filtering 和 flatmapping

filtering collector 遵循与 mapping collector 相同的模式。它是使用 Collectors.filtering() 工厂方法创建的,该方法接收常规 Predicate 来 filter 数据,同时要有必需的下游 collector。


Collectors.flatMapping() 工厂方法创建的 flatmapping collector 也是如此,它接收 flatmapping 函数(返回流的函数)和必需的下游 collector。

使用末端 collector

Collector API 还提供了几个末端操作,对应于 Stream API 上可用的末端操作。


创建自己的 collector

了解 collector 的工作原理

如前所述,Collectors工厂类仅处理对象流,因为将 collector 对象作为参数的 collect() 方法仅存在于 Stream 中。如果您需要收集数字流,那么您需要了解 collector 的组成元素是什么。


简单说,collector 建立在四个基本组件之上。前两个用于收集流的元素。第三个仅用于并行流。某些类型的 collector 需要第四个,这些 collector 需要对构建的容器作后续处理。


第一个组件用于创建收集流元素的容器。此容器易于识别。例如,在上一部分介绍的情况下,我们使用了 ArrayList 类、HashSet 类和 HashMap 类。可以使用supplier实例对创建此类容器进行建模。第一个组件称为 supplier


第二个组件旨在将流中的单个元素添加到容器。Stream API 的实现将重复调用此操作,将流的所有元素逐个添加到容器中。


在 Collector API 中,此组件由BiConsumer的实例建模。这个 biconsumer 有两个参数。


  1. 第一个是容器本身,流的先前元素填充了部分。

  2. 第二个是应添加的流元素。


此 biconsumer 在 Collector API 的上下文中称为 accumulator


这两个组件应该足以让 collector 工作,但 Stream API 带来了一个约束,使 collector 正常工作需要另外两个组件。


你可能还记得,Stream API 支持并行化。本教程稍后将更详细地介绍这一点。您需要知道的是,并行化将流的元素拆分为子流,每个元素都由 CPU 的内核处理。Collector API 可以在这样的上下文中工作:每个子流将只收集在自己的容器实例中。


处理完这些子流后,您将拥有多个容器,每个容器都包含它所处理的子流中的元素。这些容器是相同的,因为它们是与同一 supplier 一起创建的。现在,您需要一种方法将它们合并为一个。为了能够做到这一点,Collector API 需要第三个组件,即 combiner,它将这些容器合并在一起。combiner 由 BinaryOperator 的实例建模,该实例接收两个部分填充的容器并返回一个。


Stream API 的 collect() 也有个重载,这个 BinaryOperator 变成了 BiConsumer ,我们主要使用这个。


第四个组件称为 finisher,本部分稍后将介绍。

在集合中收集原始类型

使用前三个组件,您可以尝试专用数字流中的 collect() 方法。IntStream.collect() 方法有三个参数:



让我们编写代码以在List<Integer>中收集IntStream


Supplier<List<Integer>> supplier                  = ArrayList::new;//容器ObjIntConsumer<List<Integer>> accumulator         = Collection::add;//元素如何进入容器BiConsumer<List<Integer>, List<Integer>> combiner = Collection::addAll;//多个片段如何合并
List<Integer> collect = IntStream.range(0, 10) .collect(supplier, accumulator, combiner );
System.out.println("collect = " + collect);
复制代码


运行此代码将生成以下结果。


collect = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
复制代码


将这些数据收集为 Set 只需要更改supplier的实现并相应地调整类型。

在 StringBuffer 中收集原始类型

让我们研究一下如何自己实现 Collectors.joining() ,以将原始类型流的元素连接在单个字符串中。String 类是不可变的,因此无法在其中累积元素。您可以使用可变的 StringBuffer 类。


StringBuffer 中收集元素遵循与前一个相同的模式。


Supplier<StringBuffer> supplier                 = StringBuffer::new;//ObjIntConsumer<StringBuffer> accumulator        = StringBuffer::append;BiConsumer<StringBuffer, StringBuffer> combiner = StringBuffer::append;
StringBuffer collect = IntStream.range(0, 10) .collect(supplier, accumulator, combiner);
System.out.println("collect = " + collect);
复制代码


运行此代码将生成以下结果。


collect = 0123456789
复制代码

使用 finisher 对 collector 进行后续处理

你在上一段中编写的代码几乎完成了你需要的:它在 StringBuffer 实例中连接字符串,你可以通过调用它的 toString() 方法来创建一个常规的 String 对象。但是 Collectors.joining() collector 直接生成一个字符串,而无需你调用 toString()。那么它是怎么做到的呢?


Collector API 精确地定义了第四个组件来处理这种情况,称为 finisher。finisher 是一个Function,它获取累积元素的容器并将其转换为其他内容。在 Collectors.joining() 的情况下,这个函数只是下面的。


Function<StringBuffer, String> finisher = stringBuffer -> stringBuffer.toString();
复制代码


对很多 collector 来说,finisher 只是恒等函数。比如:toList()toSet()、groupingBy()toMap()。


其他情况下,collector 内部使用的可变容器成为中继容器,在返回到应用程序之前,该容器将 mapping 为其他对象(可能是另一个容器)。这就是 Collector API 处理不可变列表、set 或 map 创建的方式。finisher 用于将中继容器密封到不可变容器中,返回到应用程序。


finisher 还有其他用途,可以提高代码的可读性。Collectors 工厂类有一个工厂方法,我们还没有介绍:collectingAndThen() 方法。此方法将 collector 作为第一个参数,将 finisher 作为第二个参数。它会将第一个 collector 收集的结果,使用您提供的 finisher 对其进行 mapping。


您可能还记得以下示例,我们已经在前面的部分中多次检查过该示例。它是关于提取柱状图的最大值。


Collection<String> strings =    List.of("two", "three", "four", "five", "six", "seven", "eight", "nine",            "ten", "eleven", "twelve");
Map<Integer, Long> histogram = strings.stream() .collect( Collectors.groupingBy( String::length, Collectors.counting()));
Map.Entry<Integer, Long> maxValue = histogram.entrySet().stream() .max(Map.Entry.comparingByValue()) .orElseThrow();
System.out.println("maxValue = " + maxValue);
复制代码


第一步,您构建了 Map<Integer,Long> 类型的柱状图,在第二步中,您提取了此柱状图的最大值,按值比较键值对。


第二步实际上是将 map 转换为特殊的键/值对。您可以使用以下函数对其进行建模。


Function<Map<Integer, Long>, Map.Entry<Integer, Long>> finisher =     map -> map.entrySet().stream()              .max(Map.Entry.comparingByValue())              .orElseThrow();
复制代码


此函数的类型起初可能看起来很复杂。事实上,它只是从 map 中提取一个键值对,类型为 Map.Entry


现在您已经有了这个函数,您可以使用 collectingAndThen() 将此最大值提取步骤集成到 collector 本身中。然后,模式将变为以下内容。


Collection<String> strings =        List.of("two", "three", "four", "five", "six", "seven", "eight", "nine",                "ten", "eleven", "twelve");
Function<Map<Integer, Long>, Map.Entry<Integer, Long>> finisher = map -> map.entrySet().stream() .max(Map.Entry.comparingByValue()) .orElseThrow();//提取此finisher需要特别注意类型,消费Map,产出Entry
Map.Entry<Integer, Long> maxValue = strings.stream() .collect( Collectors.collectingAndThen( Collectors.groupingBy( String::length, Collectors.counting()), finisher ));
System.out.println("maxValue = " + maxValue);
复制代码


您可能想知道为什么需要编写此看起来非常复杂的代码?


现在,您已经拥有了由单个 collector 建模的最大值提取器,您可以将其用作另一个 collector 的下游 collector。做到这一点,可以组合更多的 collector 对您的数据进行更复杂的计算。

将两个 collector 的结果与三通 collector 相结合

在 Java SE 12 的 Collectors 类中添加了一个名为 teeing() 的方法。此方法需要两个下游 collector 和一个合并函数。


让我们通过一个场景,看看您可以使用 collector 做什么。想象一下,您有以下CarTruck两种 record。


enum Color {    RED, BLUE, WHITE, YELLOW}
enum Engine { ELECTRIC, HYBRID, GAS}
enum Drive { WD2, WD4}
interface Vehicle {}
record Car(Color color, Engine engine, Drive drive, int passengers) {}
record Truck(Engine engine, Drive drive, int weight) {}
复制代码


Car对象有几个组成部分:颜色、引擎、驱动器以及它可以运输的一定数量的乘客。Truck有引擎,有驱动器,可以运输一定量的货物。两者都实现相同的接口:Vehicle


假设您有一系列Vehicle,您需要找到所有配备电动引擎的Car。根据您的应用程序,您可能会使用流 filter 您的Car集合。或者,如果您知道下一个需求,将是找到配备混合动力引擎的Car,您可能更愿意准备一个 map,以引擎为键,并以配备该引擎的Car列表作为值。在这两种情况 API 都会为你提供正确的模式来获取所需的内容。


假设您需要将所有电动Truck添加到此集合中。也有可能想一次处理所有Vehicle,但是用于 filter 数据的 Predicate 变得越来越复杂。它可能如下所示。


Predicate<Vehicle> predicate =    vehicle -> vehicle instanceof Car car && car.engine() == Engine.ELECTRIC ||               vehicle instanceof Truck truck && truck.engine() == Engine.ELECTRIC;//这个是instanceof新用法,后面直接赋值变量,同时跟短路操作
复制代码


您真正需要的是以下内容:


  1. filterVehicle以获得所有电动Car

  2. filterVehicle以获得所有电动Truck

  3. 合并两个结果。


这正是 teeing collector 可以为您做的事情。teeing collector 由 Collectors.teeing() 工厂方法创建,该方法接收三个参数。


  1. 第一个下游 collector,用于收集流的数据。

  2. 第二个下游 collector,也用于收集数据。

  3. 一个 bifunction,用于合并由两个下游 collector 创建的两个容器。


您的数据将一次性处理,以保证最佳性能。


我们已经介绍了使用 collector 来 filter 流元素的模式。合并函数只是对 Collection.addAll() 方法的调用。以下是代码:


List<Vehicle> electricVehicles = vehicles.stream()    .collect(        Collectors.teeing(            Collectors.filtering(                vehicle -> vehicle instanceof Car car && car.engine() == Engine.ELECTRIC,                Collectors.toList()),            Collectors.filtering(                vehicle -> vehicle instanceof Truck truck && truck.engine() == Engine.ELECTRIC,                Collectors.toList()),            (cars, trucks) -> {                cars.addAll(trucks);                return cars;            }));
复制代码

实现 collector 接口

为什么要实现 collector 接口?

有三种方法可以创建自己的 collector。


包括将现有 collector 与Collectors工厂类结合,将 collector 作为下游 collector 传递给另一个 collector,或者作为 finisher 一起使用 collectingAndThen() 。我们上面教程中已经介绍过。


您还可以调用 collect() 方法,该方法接收构建 collector 的三个元素。这些方法在原始类型流和对象流上都可用。他们接收了我们在前面部分中提出的三个参数。


  1. 用于创建可变容器的 supplier,其中累积了流的元素。

  2. accumulator,由 biconsumer 建模。

  3. combiner 也由 biconsumer 建模,用于组合两个部分填充的容器,用于并行流的情况。


第三种方法是自己实现 Collector 接口,并将您的实现传递给我们已经介绍过的 collect() 方法。实现自己的 collector 可以为您提供最大的灵活性,但也更具技术性。

了解 collector 的参数类型

让我们检查一下这个接口的参数。


interface Collector<T, A, R> {        // content of the interface}
复制代码


让我们首先检查以下类型:T,R


第一种类型是 ,它对应于此 collector 正在处理的流元素的类型。T


最后一个类型是 ,它是此 collector 生成的类型。R


比如在 Stream 实例上调用的 toList() collector,类型RList。它 toSet() collector 将是 Set


groupingBy() 方法接收一个函数作参数,来计算返回 map 的键。如果用它收集 Stream,则需要传递一个对T实例作 mapping 的函数。因此,生成的 map 的类型将为 Map<K,List<T>>。也就是R的类型。


A类型处理起来比较复杂。您可能已尝试使用 IDE 来存储您在前面的示例中创建的 collector 之一。如果这样做,您可能意识到 IDE 没有为此类型提供显式值。以下示例就是这种情况。


Collection<String> strings =        List.of("two", "three", "four", "five", "six", "seven", "eight", "nine",                "ten", "eleven", "twelve");
Collector<String, ?, List<String>> listCollector = Collectors.toList();List<String> list = strings.stream().collect(listCollector);
Collector<String, ?, Set<String>> setCollector = Collectors.toSet();Set<String> set = strings.stream().collect(setCollector);
Collector<String, ?, Map<Integer, Long>> groupingBy = Collectors.groupingBy(String::length, Collectors.counting());Map<Integer, Long> map = strings.stream().collect(groupingBy);
复制代码


对于所有这些 collector,第二个参数类型仅为 ?


如果需要实现Collector接口,则必须为A提供个显式值。A是此 collector 使用的中继可变容器的实际类型。对于 toList() collector,它将是 ArrayList,对于 toSet() collector,它将是 HashSet。事实上,此类型被 toList() 的返回类型隐藏了,这就是为什么在前面的示例中无法将 ?类型替换为 ArrayList 的原因。


即使内部可变容器是由实现直接返回的,也可能发生类型AR不同的情况。例如, toList() ,您可以通过修改 ArrayList<T> 和 List<T> 来实现 Collector><T,A,R> 接口。

了解 collector 的特征

collector 定义了内部特征,流实现用它来优化 collector 使用。


有三个。


  1. IDENTITY_FINISH指示此 collector 的 finisher 是恒等函数。该实现不会为具有此特征的 collector 调用 finisher。

  2. UNORDERED指示此 collector 不保留它处理流元素的顺序。toSet() collector 就是这种情况。而toList() 就没有。

  3. CONCURRENT 特性表示 accumulator 用来存储已处理元素的容器支持并发访问。这一点对于并行流很重要。


这些特征在 collectorCollector.Characteristics枚举中定义,并由Collector接口的 characteristics() 方法以 set 返回。

实现 toList() 和 toSet() collector

使用这些元素,您现在可以重新创建类似于 toList() collector 的实现。


class ToList<T> implements Collector<T, List<T>, List<T>> {

public Supplier<List<T>> supplier() { return ArrayList::new; }
public BiConsumer<List<T>, T> accumulator() { return Collection::add; }
public BinaryOperator<List<T>> combiner() { return (list1, list2) -> {list1.addAll(list2); return list1; }; }
public Function<List<T>, List<T>> finisher() { return Function.identity(); }
public Set<Characteristics> characteristics() { return Set.of(Characteristics.IDENTITY_FINISH);//不调用finisher }}
复制代码


可以使用以下模式使用此 collector。


Collection<String> strings =        List.of("one", "two", "three", "four", "five") ;
List<String> result = strings.stream().collect(new ToList<>());System.out.println("result = " + result);
复制代码


此代码打印出以下结果。


result = [one, two, three, four, five]
复制代码


实现一个类似于 toSet() 的 collector 只需要两处修改。


实现 joining() collector

重新创建此 collector 的实现很有趣,因为它只对字符串进行操作,并且它的 finisher 不是恒等函数。


此 collector 在 StringBuffer 实例中累积它处理的字符串,然后调用 toString() 方法以生成 final 结果。


此 collector 的特征集为空。它确实保留了处理元素的顺序(因此没有 UNORDERED 特征),它的 finisher 不是恒等函数,并且不能并发使用。


让我们看看如何实现这个 collector。


class Joining implements Collector<String, StringBuffer, String> {
public Supplier<StringBuffer> supplier() { return StringBuffer::new; }
public BiConsumer<StringBuffer, String> accumulator() { return StringBuffer::append; }
public BinaryOperator<StringBuffer> combiner() { return StringBuffer::append; }
public Function<StringBuffer, String> finisher() {//会调用 return Object::toString; }
public Set<Characteristics> characteristics() { return Set.of(); }}
复制代码


您可以在以下示例中看到如何使用此 collector。


Collection<String> strings =        List.of("one", "two", "three", "four", "five") ;
String result = strings.stream().collect(new Joining());System.out.println("result = " + result);
复制代码


运行此代码将生成以下结果。


result = onetwothreefourfive
复制代码


要支持分隔符、前缀和后缀可以使用 StringJoiner

使用 Optional

创建 Optional 对象

Optional 类是具有私有构造函数的 final 类。因此,创建它实例的唯一方法是调用其工厂方法之一。其中有三个。


  1. 您可以通过调用 Optional.empty() 创建一个空的 Optional。

  2. 您可以通过调用 Optional.of() 将某元素作为参数。不允许将 null 传递给此方法。这种情况下,您将获得一个 NullPointerException

  3. 您可以通过调用 Optional.ofNullable() 将某元素作为参数。可以将 null 传递给此方法。这种情况下,您将获得一个空的 Optional。


这些是创建此类实例的唯一方法。如您所见,不能将 null 直接赋给 Optional 对象。打开非空 Optional 将始终返回非 null。


Optional<T> 有三个等效的类,用于专用数字流:OptionalIntOptionalLongOptionalDouble。这些类是原始类型(即值)的包装器。ofNullable() 方法对这些类没有意义,因为原始值不能为 null。

打开 Optional 对象

有几种方法可以使用 Optional 元素并访问它包装的元素(如果有)。你可以直接查询你拥有的实例,如果里面有东西,就打开它,或者你可以在上面使用类似流的方法: map(), flatMap(), filter(),甚至是 forEach() 的等价物。


打开 Optional 以获取其内容时应谨慎,因为如果 Optional 为空,它将引发 NoSuchElementException。除非您确定 Optional 元素中存在元素,否则应首先通过测试来保护此操作。


有两种方法可供您测试 Optional 对象:isPresent()isEmpty(),它们在 Java SE 11 中添加。


然后,要打开您的 Optional,您可以使用以下方法。



您还可以提供一个对象,如果 Optional 对象为空,将返回该对象。



最后,如果此 Optional 为空,则可以创建另一个 Optional。


  • or(supplier<Optional>supplier):如果它不为空,则返回此未修改的 Optional,如果空,则调用提供的 supplier。此 supplier 创建另一个 Optional 供方法返回。

处理 Optional 对象

Optional 类还提供模式,以便您可以将 Optional 对象与流处理集成。它具有直接对应 Stream API 的方法,您可以使用这些方法以相同的方式处理数据,并且将与流无缝集成。这些方法是 map(), filter(),和flatMap(),前两个接收的参数与 Stream API 中的方法相同,后者的函数参数需要返回Optional<T>而不是Stream


这些方法按以下规则返回 Optional 对象。


  1. 如果调用的对象为空,则返回 Optional。

  2. 如果不为空,则它们的参数、函数或 Predicate 将应用于此 Optional 的内容。将结果包装在另一个 Optional 中返回。


使用这些方法可以在某些流模式中生成更具可读性的代码。


假设您有一个具有id属性的Customer实例列表。您需要查找具有给定 ID 的客户的名称。


您可以使用以下模式执行此操作。


String findCustomerNameById(int id){    List<Customer> customers = ...;
return customers.stream() .filter(customer->customer.getId() == id); .findFirst()//返回Optional .map(Customer::getName) .orElse("UNKNOWN");}
复制代码


您可以看到 map() 方法来自 Optional 类,它与流处理很好地集成在一起。你不需要检查 findFirst() 方法返回的 Optional 对象是否为空;调用map()实际上可以为您执行此操作。

找出发表文章最多的两位联合作者

让我们看另一个更复杂的示例。通过此示例,向您展示 Stream API、Collector API 和 Optional 对象的几种主要模式。


假设您有一组需要处理的文章。一篇文章有标题、发表年份和作者列表。作者有一个名字。


您的列表中有很多文章,您需要知道哪些作者一起联合发表了最多的文章。


您的第一个想法可能是为文章构建一对作者的流。这实际上是文章和作者集的笛卡尔乘积。您并不需要此流中的所有对。您对两位作者实际上是同一对的情况不感兴趣;一对作者(A1,A2)与(A2A1)实际相同。若要实现此约束,可以添加约束条件,声明一对作者时,声明作者按字母顺序排序。


让我们为这个模型写两条 record。


record Article (String title, int inceptionYear, List<Author> authors) {}
record Author(String name) implements Comparable<Author> {
public int compareTo(Author other) { return this.name.compareTo(other.name); }}
record PairOfAuthors(Author first, Author second) { public static Optional<PairOfAuthors> of(Author first, Author second) {//用Optional实现了排序后的创建 if (first.compareTo(second) > 0) { return Optional.of(new PairOfAuthors(first, second)); } else { return Optional.empty(); } }}
复制代码


PairOfAuthorsrecord 中的创建工厂方法,可以控制哪些实例是允许的,并防止不需要的创建。若要表明此工厂方法可能无法生成结果,可以将其包装在 Optional 方法中。这完全尊重了以下原则:如果无法生成结果,则返回一个空的 optional。


让我们编写一个函数,为给定的文章创建一个 Stream<PairOfAuthors> 。您可以用两个嵌套流生成笛卡尔乘积。


作为第一步,您可以编写一个 bifunction,从文章和作者创建此流。


BiFunction<Article, Author, Stream<PairOfAuthors>> buildPairOfAuthors =    (article, firstAuthor) ->        article.authors().stream().flatMap(//对每个author都遍历authors创建作者对,生成Stream<PairOfAuthors>            secondAuthor -> PairOfAuthors.of(firstAuthor, secondAuthor).stream());//Optional的Stream
复制代码


此 bifunction 从 firstAuthorsecondAuthor 创建一个 Optional 对象,取自基于文章作者构建的流。您可以看到 stream() 方法是在 of() 方法返回的 Optional 对象上调用的。如果 Optional 流为空,则返回的流为空,否则仅包含一对作者。此流由 flatMap() 方法处理。此方法打开流,空的流将消失,并且只有有效的对将出现在生成的流中。


您现在可以构建一个函数,该函数使用此 bifunction 从文章中创建作者对流。


Function<Article, Stream<PairOfAuthors>> toPairOfAuthors =    article ->    article.authors().stream()                     .flatMap(firstAuthor -> buildPairOfAuthors.apply(article, firstAuthor));
复制代码


找到联合发表最多的两位作者可以通过柱状图来完成,柱状图中的键是作者对,值是他们一起写的文章数。


您可以使用 groupingBy() 构建柱状图。让我们首先创建一对作者的流。


Stream<PairOfAuthors> pairsOfAuthors =    articles.stream()            .flatMap(toPairOfAuthors);
复制代码


此流的构建方式是,如果一对作者一起写了两篇文章,则这对作者在流中出现两次。因此,您需要做的是计算每个对在此流中出现的次数。这可以通过 groupingBy() 来完成,其中分类器是恒等函数:对本身。此时,这些值是您需要计数的对列表。所以下游 collector 只是 counting() collector。


Map<PairOfAuthors, Long> numberOfAuthorsTogether =    articles.stream()            .flatMap(toPairOfAuthors)//所有文章的Stream<PairOfAuthors>            .collect(Collectors.groupingBy(                    Function.identity(),                    Collectors.counting()//<PairOfAuthors, Long>            ));
复制代码


找到一起发表文章最多的作者包括提取此 map 的最大值。您可以为此处理创建以下函数。


Function<Map<PairOfAuthors, Long>, Map.Entry<PairOfAuthors, Long>> maxExtractor =    map -> map.entrySet().stream()                         .max(Map.Entry.comparingByValue())                         .orElseThrow();
复制代码


此函数在 Stream.max() 方法返回的 Optional 对象上调用 orElseThrow() 方法。


这个 Optional 对象可以为空吗?要使其为空,map 本身必须为空,这意味着原始流中没有成对的作者。只要您至少有一篇文章至少有两位作者,那么这个 Optional 就不为空。

找出每年发表文章最多的两位联合作者

让我们更进一步,想知道您是否可以根据年份进行相同的处理。事实上,如果能使用单个 collector 实现,接下来就可以将其作为下游 collector 传递给 groupingBy(Article::inceptionYear)


对 map 后续提取最大值可以使用collectingAndThen()。此模式已在上一节“使用 finisher 对 collector 进行后续处理”中介绍过。此 collector 如下。


让我们提取 groupingBy() collector 和 finisher。如果使用 IDE 键入此代码,可以获取 collector 的正确类型。


Collector<PairOfAuthors, ?, Map<PairOfAuthors, Long>> groupingBy =        Collectors.groupingBy(                Function.identity(),                Collectors.counting()        );
Function<Map<PairOfAuthors, Long>, Map.Entry<PairOfAuthors, Long>> finisher = map -> map.entrySet().stream() .max(Map.Entry.comparingByValue()) .orElseThrow();
复制代码


现在,您可以将它们合并到单个 collectingAndThen() 中。将 groupingBy() 作为为第一个参数,将finisher作为第二个。


Collector<PairOfAuthors, ?, Map.Entry<PairOfAuthors, Long>> pairOfAuthorsEntryCollector =    Collectors.collectingAndThen(            Collectors.groupingBy(                Function.identity(),                Collectors.counting()            ),            map -> map.entrySet().stream()                      .max(Map.Entry.comparingByValue())                      .orElseThrow()    );
复制代码


现在,您可以使用初始 flatmap 操作和此 collector 编写完整模式。


Map.Entry<PairOfAuthors, Long> numberOfAuthorsTogether =    articles.stream()            .flatMap(toPairOfAuthors)            .collect(pairOfAuthorsEntryCollector);
复制代码


多亏了 flatMapping(),您可以通过合并中继 flatMap() 和末端 collector 来使用单个 collector 编写此代码。以下代码等效于上一个代码。


Map.Entry<PairOfAuthors, Long> numberOfAuthorsTogether =    articles.stream()            .collect(                Collectors.flatMapping(                    toPairOfAuthors,                    pairOfAuthorsEntryCollector));
复制代码


找到每年发表最多的两位联合作者,只需将这个 flatMapping() 作为下游 collector 传递给正确的 groupingBy() 即可。


Collector<Article, ?, Map.Entry<PairOfAuthors, Long>> flatMapping =     Collectors.flatMapping(            toPairOfAuthors,            pairOfAuthorsEntryCollector));
Map<Integer, Map.Entry<PairOfAuthors, Long>> result = articles.stream() .collect( Collectors.groupingBy( Article::inceptionYear, flatMapping ) );
复制代码


你可能还记得,在这个flatMapping()的深处,有一个对Optional.orElseThrow()的调用。在这个的模式中,很容易检查此调用是否会失败,因为此时有一个空的 Optional 很容易猜到。


现在我们已将此 collector 用作下游 collector,情况就不同了。你怎么能确定,每年至少有一篇文章由至少两位作者撰写?保护此代码免受任何 NoSuchElementException 的影响会更安全。

避免打开 Optional

在第一个上下文中可以接受的模式现在更加危险。处理它包括首先不要调用orElseThrow()。


这种情况下,collector 将变为以下项。它不是创建一对作者和一长串数字的键值对,而是将结果包装在一个 Optional 对象中。


Collector<PairOfAuthors, ?, Optional<Map.Entry<PairOfAuthors, Long>>>         pairOfAuthorsEntryCollector =            Collectors.collectingAndThen(                Collectors.groupingBy(                    Function.identity(),                    Collectors.counting()                ),                map -> map.entrySet().stream()                          .max(Map.Entry.comparingByValue())            );
复制代码


请注意,orElseThrow() 不再被调用,从而导致 collector 的签名中有一个 Optional。


这个 Optional 也出现在 flatMapping() collector 的签名中。


Collector<Article, ?, Optional<Map.Entry<PairOfAuthors, Long>>> flatMapping =        Collectors.flatMapping(                toPairOfAuthors,                pairOfAuthorsEntryCollector        );
复制代码


使用此 collector 会创建一个类型为 Map<Integer, Optional<Map.Entry<PairOfAuthors, Long>>> 类型的 map,我们不需要这种类型:拥有一个值为 Optional 的 map 是无用的,而且可能很昂贵。这是一种反模式。不幸的是,在计算此最大值之前,您无法猜测此 Optional 是否为空。


构建此中继 map 后,您需要删除空的 Optional 来构建表示所需柱状图的 map。我们将使用与之前相同的技术:在flatMap() 中调用 Optional 的stream())方法,以便 flatMap() 操作静默删除空的 Optional。


模式如下。


Map<Integer, Map.Entry<PairOfAuthors, Long>> histogram =    articles.stream()            .collect(                Collectors.groupingBy(                        Article::inceptionYear,                        flatMapping                )            )  // Map<Integer, Optional<Map.Entry<PairOfAuthors, Long>>>            .entrySet().stream()            .flatMap(                entry -> entry.getValue()//如果Optional为空,会成为空流,从而安全跳过                              .map(value -> Map.entry(entry.getKey(), value))                              .stream())            .collect(Collectors.toMap(                    Map.Entry::getKey, Map.Entry::getValue            )); // Map<Integer, Map.Entry<PairOfAuthors, Long>>
复制代码


请注意此模式中的 flatmap 函数。它接受一个entry作参数 ,类型为 Optional<Map.Entry<PairOfAuthors, Long>> ,并在此 Optional 上调用 map()。


如果 Optional 为空,则此调用返回空的 Optional。然后忽略 map 函数。接下来调用 stream() 返回一个空流,该流将从主流中删除,因为我们处于 flatMap() 调用中。


如果 Optional 中有一个值,则使用此值调用 map 函数。此 map 函数创建一个具有相同键和此现有值的新键值对。此键值对的类型为 Map.Entry,并且通过此 map() 方法将其包装在 Optional 对象中。对 stream() 的调用会创建一个包含此 Optional 内容的流,然后由 flatMap() 调用打开该流。


此模式用空的 Optional 将 Stream<Map.Entry<Integer, Optional<Map.Entry<PairOfAuthors, Long>>>>mapping 为 Stream<Map.Entry<Integer, Map.Entry<PairOfAuthors, Long>>>,删除所有具有空 Optional 的键/值对。


使用 toMap() collector 可以安全地重新创建 map,因为您知道在此流中不能使用两次相同的键。


此模式使用了 Optional 和 Stream API 的三个要点。


  1. Optional.map() 方法,如果在空 Optional 上调用,则返回空的 Optional。

  2. Optional.stream() 方法,该方法在 Optional 的内容上打开流。如果 Optional 为空,则返回的流也为空。它允许您从 Optional 空间无缝的移动到流空间。

  3. Stream.flatMap() 方法,用于打开从 Optional 构建的流,以静默方式删除空流。

消费 Optional 的内容

Optional 类还具有两个将 Consumer 作为参数的方法。


烧哥总结

(验证中,代码库持续更新)


lambda 将匿名类换成了匿名方法,能代表某个操作,让代码更直观(语法糖),但良好的命名很重要。


改写为 lambda 首先得是函数接口,Operator 是 Function 的简化版。


可以序列化,从而可以作为字段、方法参数和返回类型,实现了方法引用、链式调用、函数式编程。


lambda 已经深入 JDK 内部,所以性能方面很关注,为避免装箱拆箱,提供了很多原生类型专用版,但有时候要手动装箱。


为了性能,避免在内存中处理大量数据,同时也提高可读性,出现了 Stream API。


流处理的整个过程最好都是流,所以有 flatmap、mapMulti 各种中继操作,


甚至末端 collector 也可以有下游 collector,甚至 collector 可以串联、三通,比如神奇的Collectors.flatMapping()


流不应该作为变量或参数。


流中不应该改变外围变量,会捕获外界变量,降低处理性能,也会把并行流变成多线程并发。


每次中继操作都产生一个新流。


同样为了性能,reduce 可以并行,但要具有可结合性、要有幺元。如果幺元未知,会返回 Optional。


三参数的 reduce 组合了 mapping 过程。


专用数字流的 sum、min、max、count、average、summaryStatistics 为末端操作。


转换为流的源如果用 Set,会是乱序的。


map、flatmap 会删除 SORTED、DISTINCTED、NONNULL。


本教程未详细说明的:spliterator、不可变流、并发流。


Stream.collect(Collectors.toList()) 只能用于对象流,数字流要么装箱,要么用三参数那个,或者自定义 collector,五个参数。


flatmap 会跳过空流,包括Optional.stream()产生的流,所以看到 Optional,不要orElseThrow(),可以用 flatmap 取出。


API 是看起来越来越复杂,Collectors.mapping()


public static <T,U,A,R> Collector<T,?,R> mapping(Function<? super T,? extends U> mapper, Collector<? super U,A,R> downstream)
复制代码


方法名前面四个,返回类型里三个,还有问号,参数里 super 了三个。


Map<City, Set<String>> lastNamesByCity   = people.stream().collect(     groupingBy(Person::getCity,                mapping(Person::getLastName,                        toSet())));
复制代码


虽然很好用。

发布于: 刚刚阅读数: 5
用户头像

烧霞

关注

还未添加个人签名 2020-08-26 加入

一步一步 架构师之路

评论

发布
暂无评论
从头学Java17-Stream API(二)结合Record、Optional_Optional_烧霞_InfoQ写作社区