乐趣区

关于java17:从头学Java17Stream-API二结合RecordOptional

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 = 3
countWithACollector = 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 :: 4
4 :: 3
5 :: 3
6 :: 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(",")));// 变成 String
map.forEach((key, value) -> System.out.println(key + "::" + value));

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

3 :: one, two, six, ten
4 :: four, five, nine
5 :: three, seven, eight
6 :: 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, ten
4 :: four, five, nine
5 :: three, seven, eight
6 :: 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 =>3
4 :: 3
5 :: 3
6 :: 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 :: 3
4 :: 3
5 :: 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 上可用的末端操作。

  • maxBy() 和) minBy()。)这两个办法都将 comparator 作为参数,如果解决的流自身为空,则返回一个 Optional 对象。
  • summingInt)()、summingLong()) 和 summingDouble()。)这三种办法将 mapping 函数作为参数,别离将流的元素 mapping 为intlongdouble,而后对它们求和。
  • averageagingInt())、averageagingLong())和averageagingDouble()). 这三种办法也将 mapping 函数作为参数,在计算平均值之前别离将流的元素 map 为 intlongdouble。这些 collector 的工作形式与 IntStreamLongStreamDoubleStream 中定义的相应 average()) 办法不同。它们都返回一个 Double 实例,对于空流返回 0。而数字流的average()) 办法返回一个 Optional 对象,该对象对于空流为空。

创立本人的 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()) 办法有三个参数:

  • Supplier的实例,称为 supplier;
  • ObjIntConsumer的实例,称为accumulator;
  • BiConsumer的实例,称为combiner

让咱们编写代码以在 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 只须要两处批改。

  • supplier()) 办法将返回 HashSet::new
  • characteristics()) 办法会将 Features.UNORDERED 增加到返回的 set 中。

实现 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,您能够应用以下办法。

  • get()):此办法已被弃用,因为 is 看起来像一个 getter,但如果 Optional 为空,它能够抛出 NoSuchElementException
  • orElseThrow()) 是自 Java SE 10 以来的首选模式。它与 get()) 办法雷同,但它的名称毫无疑问它能够抛出NoSuchElementException
  • orElseThrow(Supplier exceptionSupplier):)与后面的办法雷同。它应用您传递的 supplier 作为参数来创立它引发的异样。

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

  • orElse(T returnObject):)如果在空的 Optional 值上调用,则返回参数。
  • orElseGet(Supplier supplier):)与前一个雷同。实际上,仅在须要时调用所提供的 supplier。

最初,如果此 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 作为参数的办法。

  • ifPresent(Consumer consumer)):此办法应用此 Optional 的内容(如果有)调用提供的 Consumer。它实际上等同于 Stream.forEach(Consumer)) 办法。
  • ifPresentOrElse(Consumer consumer,Runnable runnable):)如果 Optional 非空,此办法与前一个办法雷同。如果空,则调用提供的 Runnable 实例。

烧哥总结

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

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

尽管很好用。

退出移动版