Java8实战-函数式数据处理-Steam流之收集器接口

Collector接口包含了一系列方法,为实现具体的归约操作(即收集器)提供了范本。
Collector接口中实现的许多收集器,例如toList或groupingBy。可以为Collector接口提供自己的实现,从而自由地创建自定义归约操作。


一. Collector接口

下面的代码为Collector接口的定义,列出了接口的签名以及声明的五个方法。
在这里插入图片描述

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

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

import java.util.List;
import java.util.Set;
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 null;
    }

    @Override
    public BiConsumer<List<T>, T> accumulator() {
        return null;
    }

    @Override
    public BinaryOperator<List<T>> combiner() {
        return null;
    }

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

    @Override
    public Set<Characteristics> characteristics() {
        return null;
    }
}

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

  1. 建立新的结果容器:supplier方法
    /**
     * A function that creates and returns a new mutable result container.
     *
     * @return a function which returns a new, mutable result container
     */
    Supplier<A> supplier();

supplier方法必须返回一个结果为空的Supplier,也就是一个无参数函数,在调用时它会创建一个空的累加器实例,供数据收集过程使用。对空流执行操作的时候,这个空的累加器也代表了收集过程的结果。

ToListCollector中,supplier返回一个空的List,如下所示:

    @Override
    public Supplier<List<T>> supplier() {
        return () -> new ArrayList<>();
    }
    //或
    @Override
    public Supplier<List<T>> supplier() {
        return ArrayList::new;
    }
  1. 将元素添加到结果容器:accumulator方法
    /**
     * A function that folds a value into a mutable result container.
     *
     * @return a function which folds a value into a mutable result container
     */
    BiConsumer<A, T> accumulator();

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

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

    @Override
    public BiConsumer<List<T>, T> accumulator() {
        return (list, item) -> {
            list.add(item);
        };
    }
    //或
    @Override
    public BiConsumer<List<T>, T> accumulator() {
        return List::add;
    }    
  1. 对结果容器应用最终转换:finisher方法
    /**
     * Perform the final transformation from the intermediate accumulation type
     * {@code A} to the final result type {@code R}.
     *
     * <p>If the characteristic {@code IDENTITY_TRANSFORM} is
     * set, this function may be presumed to be an identity transform with an
     * unchecked cast from {@code A} to {@code R}.
     *
     * @return a function which transforms the intermediate result to the final
     * result
     */
    Function<A, R> finisher();

在遍历完流后,finisher方法必须返回在累积过程的最后要调用的一个函数,以便将累加器对象转换为整个集合操作的最终结果。

通常,就像ToListCollector的情况一样,累加器对象恰好符合预期的最终结果,因此无需进行转换。所以finisher方法只需返回identity函数:

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

实践中的实现细节可能还要复杂一点,一方面是因为流的延迟性质,可能在collect操作之前还需要完成其他中间操作的流水线,另一方面则是理论上可能要进行并行归约。
在这里插入图片描述

  1. 合并两个结果容器:combiner方法
    /**
     * A function that accepts two partial results and merges them.  The
     * combiner function may fold state from one argument into the other and
     * return that, or may return a new result container.
     *
     * @return a function which combines two partial results into a combined
     * result
     */
    BinaryOperator<A> combiner();

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

对于toList而言,只要把从流的第二个部分收集到的列表加到遍历第一部分时得到的列表中:

    @Override
    public BinaryOperator<List<T>> combiner() {
        return (list1, list2) -> {
            list1.addAll(list2);
            return list1;
        };
    }
  1. 原始流会以递归方式拆分为子流,直到定义流是否需要进一步拆分的一个条件为非。(如果分布式工作单位太小,并行计算往往比顺序计算要慢,而且要是生成的并行任务比处理器内核数多很多的话就毫无意义了)。
  2. 所有的子流都可以并行处理。即对每个子流应用图所示的顺序归约算法。
  3. 使用收集器combiner方法返回的函数,将所有的部分结果两两合并。这时会把原始流每次拆分时得到的子流对应的结果合并起来。
    在这里插入图片描述
  1. characteristics方法
    /**
     * Returns a {@code Set} of {@code Collector.Characteristics} indicating
     * the characteristics of this Collector.  This set should be immutable.
     *
     * @return an immutable set of collector characteristics
     */
    Set<Characteristics> characteristics();

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

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

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


二.自定义收集器接口

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::add;
    }

    @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.emptyList()这个单例(singleton)。这意味着它可安全地替代原生Java。之间的其他差异在于toList是一个工厂,而ToListCollector必须用new来实例化。

        List<Dish> menu = new ArrayList<>();//菜单;
        menu.add(new Dish(600, "麻婆豆腐", "王师傅",true));
        menu.add(new Dish(300, "爆炒猪肝", "王师傅",false));
        menu.add(new Dish(800, "油焖茄子", "李师傅",true));
        menu.add(new Dish(700, "蒜蓉龙虾", "赵师傅",false));

//自定义
        List<Dish> dishes = menu.stream()
                .filter(f -> f.getCalories() > 500)
                .collect(new ToListCollector<>());
//原生
        List<Dish> dishes2 = menu.stream()
                .filter(f -> f.getCalories() > 500)
                .collect(Collectors.toList());

进行自定义收集而不去实现Collector对于IDENTITY_FINISH的收集操作,还有一种方法可以得到同样的结果而无需从头实现新的Collectors接口。Stream有一个重载的collect方法可以接受另外三个函数——supplier、 accumulator和combiner,其语义和Collector接口的相应方法返回的函数完全相同。

        List<Dish> dishes = menu.stream()
                .filter(f -> f.getCalories() >500)
                .collect(ArrayList::new,List::add,List::addAll);

这第二种形式虽然比前一个写法更为紧凑和简洁,却不那么易读。此外,以恰当的类来实现自己的自定义收集器有助于重用并可避免代码重复。另外值得注意的是,这第二个collect方法不能传递任何Characteristics,所以它永远都是一个IDENTITY_FINISH和CONCURRENT但并非UNORDERED的收集器


over

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

筑梦的熊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值