Collectors 收集器接口

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/CmdSmith/article/details/85016549

收集器接口

你可以为Collectors接口提供自己的实现,从而自由地创建自定义归约操作。

public interface Collector<T, A, R> {
    Supplier<A> supplier();
    BiSonsumer<A, T> accumulator();
    Function<A, R> finisher();
    BinaryOperator<A> combiner();
    Set<Characteristics> characteristics();
}

本列表使用一下定义:

  • T 是流中要收集的项目的泛型
  • A 是累加器的类型,累加器是在收集过程中用于累积部分结果的对象。
  • R 是收集操作得到的对象(通常但不一定是集合)的类型。

例如你可以实现一个ToListCollector类,将Stream中的所有元素收集到一个List里,它的签名如下:

public class ToListCollector<T> implements Collector<T, List<T>, List<T>>

理解Collector 接口声明的方法

前四个方法都会返回一个被collect方法调用的函数,而第五个方法characteristics则提供了一系列特征,也就是一个提示列表,告诉collect方法在执行归约操作的时候可以应用哪些优化(比如并行化)。

1.建立新的结果容器:supplier方法

supplier方法必须返回一个结果为空的Supplier,也就是一个无参数函数,在调用时他会创建一个空的累加器实例,供数据收集过程使用。

对于将累加器本身作为结果返回的收集器,比如我们的ToListCollector,在对空流执行操作的时候,这个空的累加器也代表了收集过程的结果,在我们的ToListCollector中,supplier返回一个空的List

public Supplier<List<T>> supplier() {
    return ArrayList::new;
}

2. 将元素添加到结果容器:accumulator方法

accumulator(积聚者、收集器)方法会返回执行归约操作的函数。当遍历到流中第n个元素时,这个函数执行时会有两个参数:保存归约结果的累加器(已收集了流中的前n-1个项目),还有第n个元素本身。该函数将返回void,因为累加器是原位更新,即函数的执行改变了它的内部状态以体现遍历的元素的效果。

对于ToListCollector,这个函数仅仅会把当前项目添加至已经遍历过的项目的列表。

public BiConsumer<List<T>, T> accumulator() {
    return (list, item) -> list.add(item);
}
public BiConsumer<List<T>, T> accumulator() {
    return List::add;
}

3.对结果容器应用最终转换:finisher方法

在遍历完流后,finisher方法必须返回在累积过程中的最后要调用的一个函数,以便将累加器对象转换为整个集合的最终结果。通常,就像ToListCollector的情况一样,累加器对象恰好符合预期的最终结果,因此无需转换。所以finisher方法只需返回identity函数:

public Function<List<T>, List<T>> finisher() {
    return Function.identity();
}

以上三个方法已经足以对流进行顺序归约,至少从逻辑上看可以按图 6-7 进行。实践细节可能还要复杂一点,一方面是因为流的延迟性质,可能在collect操作之前还需要中间操作的流水线,另一方面则是理论上可能要进行并行归约。

图 6-7 顺序归约过程的逻辑步骤

                   开始
                    |
                    |
    A accumulator = collector.supplier().get();
                    | <----- collector.accumulator().accept(accumulator, next)
                    |                              ||
                    |          是                   |
            流中是否有更多的项目 ----> T next = 取流中下一个项目
                    |
                    | 否
                    |
        R result = collector.finisher().apply(accumulator);
                    |
                    |
                return result;
                    |
                    |
                   结束

4.合并结果容器:combiner方法

四个方法中的最后一个——combiner方法会返回一个供归约操作使用的函数,它定义了对流的各个子部分进行并行处理时,各个子部分归约所得的累加器要如何合并。

对于toList而言,这个方法的实现非常简单,只要把流的第二个部分收集到的项目列表加到遍历第一部分时得到的列表后面就行了:

public BinaryOperator<List<T>> combiner() {
    return (list1, list2) -> {
        list1.addAll(list2);
        return list1;
    };
}

有了这第四个方法,就可以对流进行并行归约了。它用到Java 7中引入的分支/合并框架和Spliterator抽象。

  • 原始流会以递归的方式拆分为子流,直到定义流是否需要进一步拆分的一个条件为非(如果分布式工作单位太小,并行计算往往比顺序计算要慢,而且要是生成的并行任务比处理器内核多很多的话就毫无意义了)。
  • 现在,所有子流都可以并行处理,即对每个子流应用图6-7所示的顺序归约算法。
  • 最后,使用收集器combiner方法返回的函数,将所有的部分结果两两合并,这时会把原始流每次拆分得到的子流对应的结果合并起来。

图 6-8 使用combiner方法来并行化归约过程

5. characteristics方法

最后一个方法 —— characteristics会返回一个不可变的Characteristics集合,它定义了收集器行为——尤其是流是否可以并行归约,以及可以使用哪些优化的提示。Characteristics是一个包含三个项目的枚举。

  • UNORDERED——(无序的)归约结果不受流中项目的遍历和累积顺序的影响
  • CONCURRENT——(并发的)accumulator函数可以从多个线程同时调用,且该收集器可以并行归约流。如果收集器没有标为UNORDERED,那它仅在用于无序数据源时才可以并行归约
  • IDENTITY_FINISH——这表明完成器方法返回的函数是一个恒等函数,可以跳过。这种情况下,累加器对象会直接用作归约过程的最终结果。这也意味着,将累加器A不加检查地转换为结果R是安全的。

我们迄今开发的ToListCollector是IDENTITY_FINISH的,因为用来累积流中元素的List已经是我们要的最终结果,用不着进一步转换了,但它并不是UNORDERED,因为用在有序流上的时候,我们还是希望顺序能够保留在得到的List中。最后他是CONCURRENT的,但我们刚才说过了,仅仅在背后的数据源无序时才会并行处理。。

全部融合到一起

package org.smith.study.fundamental.java8inaction.module6;

import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;

public class ToListCollector<T> implements Collector<T, List<T>, List<T>> {
    @Override
    public Supplier<List<T>> supplier() {
        return ArrayList::new;
    }

    @Override
    public BiConsumer<List<T>, T> accumulator() {
        return (list, item) -> list.add(item);
    }

    @Override
    public BinaryOperator<List<T>> combiner() {
        return (list1, list2) -> {
            list1.addAll(list2);
            return list1;
        };
    }

    @Override
    public Function<List<T>, List<T>> finisher() {
        return Function.identity();
    }

    @Override
    public Set<Characteristics> characteristics() {
        return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH, Characteristics.CONCURRENT));
    }
}

这个实现与Collectors.toList方法并不完全相同,但区别仅仅是一些小的优化。这些优化的一方面主要是Java API所提供的收集器在需要返回空列表时使用了Collections.empty()这个单例(singleton)。这意味着它可安全地替代原生Java,来收集菜单流中的所有Dish的列表:

List<Dish> dishes4 = menuStream.collect(new ToListCollector<Dish>());

这个实现和标准的

List<Dish> dishes5 = menuStream.collect(Collectors.toList());

构造之间的其他差异在于toList是一个工厂,而ToListCollector必须用new来实例化。

进行自定义收集而不去实现Collector

对于IDENTITY_FINISH的收集操作,还有一种方法可以得到相同的结果而无需从头实现新的Collectors接口。Stream有一个重载的collect方法可以接受另外三个函数——supplier、accumulator和combiner,其语义和Collector接口的相应方法返回的函数完全相同。所以比如说,我们可以像下面这样把菜肴流中的项目收集到一个List中:

menuStream.collect(ArrayList::new,  // 供应源
                List::add,  // 收集器
                List::addAll);  // 组合器

虽然第二种形式更为简洁和紧凑,但是不易读。此外以恰当的类来实现自己的自定义收集器有助于重用并可以避免代码重复。

这第二个方法永远不能传递Characteristics,所以它永远都是一个IDENTITY_FINISH和CONCURRENT但并非UNORDERED的收集器。

猜你喜欢

转载自blog.csdn.net/CmdSmith/article/details/85016549