创建自己的Collector

理解Collector的工作原理

如前所述,收集器工厂类只处理对象流,因为以收集器对象为参数的collect()方法只存在于Stream中。如果您需要收集数字流,那么您需要了解收集器的构建元素是什么。

简而言之,收集器是建立在四个基本组件之上的。前两个用于收集流的元素。只有平行流才需要第三个。第四个是某些类型的收集器所需要的,这些收集器需要对构建的容器进行后处理。

第一个组件用于创建容器,流的元素将在其中收集。这个容器很容易辨认。例如,在前一部分讨论的案例中,我们使用了ArrayList类、HashSet类或HashMap类。可以使用Supplier实例来建模创建这样的容器。第一个组件称为Supplier。

第二个组件对将单个元素从流添加到容器的过程进行建模。该操作将被Stream API的实现反复调用,以便将流的所有元素一个接一个地添加到容器中。

在Collector API中,该组件由BiConsumer的一个实例建模。这个双消费者有两个参数。

  • 第一个是容器本身,它部分地填充了流的前一个元素。
  • 第二个是流的元素,应该添加到这个部分填充的容器中。

在Collector API的上下文中,这个双消费者称为累加器。
这两个组件应该足以让收集器正常工作,但是Stream API带来了一个约束,使收集器正常工作需要另外两个组件。

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

一旦这些子流被处理,您就有了几个容器,每个容器都包含它所处理的子流中的元素。这些容器是相同的,因为它们是用相同的supplier创建的。现在,你需要一种方法将它们合并成一个。要做到这一点,收集器API需要第三个组件,一个组合器,将这些容器合并在一起。组合器由BinaryOperator实例建模,该实例接受两个部分填充的容器并返回一个。

这个BinaryOperator也由Stream API的collect()重载中的BiConsumer建模。
第四个组件称为完成器,稍后将在本部分中介绍。

在集合中收集基本类型

对于前三个组件,可以从专门的数字流中使用collect()方法。IntStream.collect()方法有三个参数:

  • Supplier的实例,称为Supplier;
  • ObjIntConsumer的一个实例,称为accumulator;
  • BiConsumer的一个实例,称为combiner。

让我们编写代码来收集List实例中的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.join()等价的函数,以在单个字符串中连接基本类型流的元素。String类是不可变的,所以不能在其中累积元素。您可以使用可变的StringBuffer类,而不是使用String类。
在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()收集器直接生成一个String,而不需要您调用toString()。那么,这是如何做到的呢?

Collector API精确地定义了第四个组件来处理这种情况,它被称为finisher。finisher是Function 的一个实例,它接受积累元素的容器并将其转换为其他元素。对于Collectors.joining(),这个函数如下所示。

Function<StringBuffer, String> finisher = stringBuffer -> stringBuffer.toString();

在许多收集器中,整理器只是恒等函数。以下收集器是这样的:toList()、toSet()、groupingBy()和toMap()。
在所有其他情况下,收集器内部使用的可变容器成为一个中间容器,在返回给应用程序之前,它将被映射到其他对象(可能是另一个容器)。这就是Collector API如何处理不可变列表、集或映射的创建。完成器用于在将中间容器返回给应用程序之前将其密封为不可变容器。

这个完成器还有其他的用途,可以提高代码的可读性。collections工厂类有一个我们还没有介绍的工厂方法:collectingAndThen()方法。此方法以collector 作为第一个参数,以finisher 作为第二个参数。它只会将这个函数应用到使用第一个收集器收集您的流并使用您提供的函数映射它所计算的结果。

您可能还记得下面这个例子,我们在前面几节中已经讨论过好几次了。它是关于提取直方图的最大值。

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的直方图,在第二步中,您提取了该直方图的最大值,按值比较键值对。

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

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

这个函数的类型一开始可能看起来很复杂。实际上,它只是从映射中提取一个键值对。它接受一个特定类型的Map实例,并从这个Map返回一个键值对,这是Map的一个实例。具有相同类型的项。

现在有了这个函数,就可以使用collectingAndThen()将最大值提取步骤集成到收集器本身中。然后,模式变成如下所示。

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

Map.Entry<Integer, Long> maxValue =
    strings.stream()
           .collect(
               Collectors.collectingAndThen(
                   Collectors.groupingBy(
                           String::length,
                           Collectors.counting()),
                   finisher
               ));

System.out.println("maxValue = " + maxValue);

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

现在您已经有了由单个收集器建模的最大值提取器,您可以将它用作另一个收集器的下游收集器。如果能够做到这一点,就可以结合更多的收集器对数据进行更复杂的计算。

使用Teeing Collector结合两个收集器的结果

在Java SE 12的Collectors类中添加了一个名为teeing()的方法。该方法采用两个下游收集器和一个合并函数。

让我们通过一个用例来看看您可以用收集器做什么。假设您有以下汽车和卡车的记录。

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) {}

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

假设您有一组车辆,您需要找到所有带有电动引擎的汽车。根据您的应用程序,您可能最终会使用流来过滤汽车集合。或者,如果您知道下一个请求将是获得带有混合引擎的汽车,那么您可能更愿意准备一个地图,以引擎作为键,以带有引擎类型的汽车列表作为值。在这两种情况下,Stream API都会给你提供正确的模式来获得你需要的东西。

假设您需要将所有的电动卡车添加到这个集合中。仍然可以在一次通过的车辆集合上创建这个联合,但用于过滤数据的谓词正变得越来越复杂。它可能看起来如下所示。

Predicate<Vehicle> predicate =
    vehicle -> vehicle instanceof Car car && car.engine() == Engine.ELECTRIC ||
               vehicle instanceof Truck truck && truck.engine() == Engine.ELECTRIC;

你真正需要的是:

  • 过滤车辆以获得所有的电动汽车
  • 把它们过滤掉,得到所有的电动卡车
  • 合并两个结果。

这正是teeing collector能为你做的。teeing collector是由接受三个参数的Collectors.teeing()工厂方法创建的。

  • downstream ,第一个下游收集器,用于收集流的数据。
  • downstream ,第二个下游收集器,也用于以独立的方式收集数据。
  • bifunction,用于合并由两个下游收集器创建的两个容器。

您的数据在一个过程中被处理,以保证最佳性能。
我们已经介绍了可以使用收集器过滤流中的元素的模式。合并函数只是对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;
            }));

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值