Java8 Stream源码精讲(五):爆肝近万字,带你重识Collector

2,696 阅读19分钟

简介

Java8 Stream源码精讲(一):从一个简单的例子入手
Java8 Stream源码精讲(二):Stream创建原理深度解析
Java8 Stream源码精讲(三):中间操作原理详解
Java8 Stream源码精讲(四):一文说透四种终止操作

在上一篇文章,通过阅读源码,详细分析了终止操作是怎样划分为非短路操作和短路操作的,然后讲解了ForEachOp、ReduceOp、MatchOp和FindOp这四种TerminalOp实现,带领大家深入各个终止操作方法的原理,理解如何通过终止操作触发Stream上编排的中间操作和终止操作逻辑。在分析collect()方法时说到,这个终止操作方法是通过Collector与ReduceOp组合实现的。本章我们将深入collector内部,研究它是如何与ReduceOp相互协作完成收集工作的,以及Collectors工厂类的内部原理。

接口定义

Collector译为收集器,它提供了四个函数,用于将Stream中的元素作为输入并入到可变的容器当中,并且应用函数将可变容器转换为最终结果。这样解释可能不容易理解,下面对它的接口定义进行说明,然后结合ReduceOp了解它是如何工作的,大家应该就明白了。

public interface Collector<T, A, R> {
    
    Supplier<A> supplier();

    BiConsumer<A, T> accumulator();

    BinaryOperator<A> combiner();

    Function<A, R> finisher();

    Set<Characteristics> characteristics();
}

Collector定义了5个方法,其中四个方法的返回类型都是函数:

  • supplier()方法:返回一个Supplier类型的函数,这个函数用于创建和返回一个新的可变的用于存放结果的容器。
  • accumulator()方法:返回一个BiConsumer类型的函数,这个函数的作用是将Stream中的一个元素并入到用于存放结果的可变容器中。
  • combiner()方法:返回一个BinaryOperator类型的函数,用于将两个保存结果的容器合并成一个新的容器。这个函数在并行流处理时会调用,不用过多关注。
  • finisher()方法:返回一个Function类型的函数,将存放结果的可变容器转换成最终结果。
  • characteristics()方法:返回一个不可变的Set集合,这个集合表示该Collector的特征,类型是Characteristics。

Characteristics是一个枚举类,我们看看它定义了哪些类型:

enum Characteristics {
    
    CONCURRENT,

    UNORDERED,

    IDENTITY_FINISH
}
  • CONCURRENT:表示当前的Collector是支持并发的,可以在多线程中并发安全地调用accumulator函数收集结果到可变容器中。
  • UNORDERED:表示当前Collector收集到容器中的元素顺序不一定跟Stream中元素顺序是一致的,比如收集结果到Set集合中。
  • IDENTITY_FINISH:表示当前Collector中的finisher函数只是一个标记函数,它可以被忽略,因为保存结果的可变函数可以强转为最终结果。

唯一实现

Collector只有一个实现类,就是Collectors的内部类CollectorImpl。

EJSY8VL7@SQ@_}2ML5OM(M2.png

CollectorImpl非常简单,就是创建CollectorImpl时,调用方需要提供四种类型的函数和表示Collector特征的Set集合,CollectorImpl只是通过变量保存这些信息:

//省略了不重要的代码
static class CollectorImpl<T, A, R> implements Collector<T, A, R> {
    private final Supplier<A> supplier;
    private final BiConsumer<A, T> accumulator;
    private final BinaryOperator<A> combiner;
    private final Function<A, R> finisher;
    private final Set<Characteristics> characteristics;

    CollectorImpl(Supplier<A> supplier,
                  BiConsumer<A, T> accumulator,
                  BinaryOperator<A> combiner,
                  Function<A,R> finisher,
                  Set<Characteristics> characteristics) {
        this.supplier = supplier;
        this.accumulator = accumulator;
        this.combiner = combiner;
        this.finisher = finisher;
        this.characteristics = characteristics;
    }
}

协同工作

介绍完了Collector接口定义,下面我们看看它的方法是在什么地方被调用的。
ReferencePipeline#collect():

public final <R, A> R collect(Collector<? super P_OUT, A, R> collector) {
    A container;
    //并行流的逻辑,不用关心
    if (isParallel()
            && (collector.characteristics().contains(Collector.Characteristics.CONCURRENT))
            && (!isOrdered() || collector.characteristics().contains(Collector.Characteristics.UNORDERED))) {
        container = collector.supplier().get();
        BiConsumer<A, ? super P_OUT> accumulator = collector.accumulator();
        forEach(u -> accumulator.accept(container, u));
    }
    else {
        //串行流调用的方法,之前分析过,这里返回的container就是上面分析的保存元素结果的可变容器
        container = evaluate(ReduceOps.makeRef(collector));
    }
    //如果collector被IDENTITY_FINISH标记,则直接强转为最终结果;否则调用finisher函数转换结果
    return collector.characteristics().contains(Collector.Characteristics.IDENTITY_FINISH)
           ? (R) container
           : collector.finisher().apply(container);
}

从collect()方法中可以看到,collector的finisher函数是用于将容器转换为最终结果的,如果Collector#characteristics()返回的Set集合包含了IDENTITY_FINISH类型,则不会调用finisher函数,而是直接强转为最终类型。这也是为什么上面说IDENTITY_FINISH表示当前Collector中的finisher函数只是一个标记函数,可以被忽略。

进入ReduceOps#makeRef()方法:

public static <T, I> TerminalOp<T, I>
makeRef(Collector<? super T, I, ?> collector) {
    //用于创建和返回保存结果的可变容器的函数
    Supplier<I> supplier = Objects.requireNonNull(collector).supplier();
    //用于将元素并入可变容器的函数
    BiConsumer<I, ? super T> accumulator = collector.accumulator();
    //合并两个容器,返回一个新的容器的函数
    BinaryOperator<I> combiner = collector.combiner();
    class ReducingSink extends Box<I>
            implements AccumulatingSink<T, I, ReducingSink> {
        @Override
        public void begin(long size) {
            //Stream元素到达sink之前,先调用supplier返回可变容器,赋值给state变量
            state = supplier.get();
        }

        @Override
        public void accept(T t) {
            //每次从上一个sink传递过来一个元素,调用accumulator函数将元素并入到可变容器state中
            accumulator.accept(state, t);
        }

        @Override
        public void combine(ReducingSink other) {
            //合并结果,并行处理调用,不关心
            state = combiner.apply(state, other.state);
        }
    }
    return new ReduceOp<T, I, ReducingSink>(StreamShape.REFERENCE) {
        @Override
        public ReducingSink makeSink() {
            return new ReducingSink();
        }

        @Override
        public int getOpFlags() {
            return collector.characteristics().contains(Collector.Characteristics.UNORDERED)
                   ? StreamOpFlag.NOT_ORDERED
                   : 0;
        }
    };
}

ReduceOps#makeRef()方法中的代码逻辑很清楚地解释了Collector中函数是如何被ReducingSink调用的。可以很清晰地看到,在Sink#begin()方法中调用Collector的supplier函数创建可变容器,并保存在state变量中;在Sink#accept()方法中调用Collector的accumulator函数将元素合并到可变容器中。

Collectors

在使用Stream API时,我们通常不会直接实现Collector接口,而是通过Collectors创建Collector来满足我们的逻辑需要。Collectors是Collector的工厂类,它内部实现了非常多的静态方法,用于返回具有不同功能的Collector。下面我根据功能类型将他们划分为如下六类:

功能分类方法
字符操作joining()
转换操作mapping()
收集操作toCollection() toList() toSet() toMap() toConcurrentMap()
分组分区groupBy() groupByConcurrent() partionBy()
折叠操作reducing() maxBy() minBy() counting()
数值计算averagingDouble() averagingInt() averagingLong() summingDouble() summingInt() summingLong() summarizingDouble() summarizingInt() summarizingLong()

字符操作

joining()方法

joining()方法,返回一个Collector,这个Collector能够将Stream中的元素按照顺序连接成一个字符串返回。它有三个重载方法,我们分别来看一下:

  • 不带参数的重载方法 先来看一个使用joining()方法的例子
String result = Stream.of("java", "scala", "go", "python")
        .collect(Collectors.joining());
System.out.println(result);

很明显,在控制台的输出是下面这个结果,它将元素拼接成一个新的String类型,元素之间没有间隔符。

javascalagopython

进入Collectors#joining()探究它是如何实现的:

public static Collector<CharSequence, ?, String> joining() {
    return new CollectorImpl<CharSequence, StringBuilder, String>(
            StringBuilder::new, StringBuilder::append,
            (r1, r2) -> { r1.append(r2); return r1; },
            StringBuilder::toString, CH_NOID);
}

Collectors#joining()返回一个CollectorImpl实例,CollectorImpl在上面有提到,它是Collector的唯一实现,包括下面要分析的所有方法都是返回一个CollectorImpl对象,所以我们重点关注构造参数。

在Collectors#joining()中,Collector的supplier函数是StringBuilder::new,这是一个方法引用,说明是将Stream的元素保存到一个StringBuilder对象中的;Collector的accumulator函数是StringBuilder::append,意味着每一个元素都会被StringBuilder#append()方法追加到StringBuilder中;Collector的finisher函数是StringBuilder::toString,表示是将上面的StringBuilder转化为String作为最终结果返回;CH_NOID表示Collector没有任何特征。

static final Set<Collector.Characteristics> CH_NOID = Collections.emptySet();
  • 带delimiter分隔符的重载方法 这个方法返回的Collector是将Stream中的元素按照顺序以delimiter分隔符拼接到一起,还是来看一个例子:
String result = Stream.of("java", "scala", "go", "python")
        .collect(Collectors.joining(","));
System.out.println(result);

结果跟上面有稍许不同,元素与元素之间是以分隔符“,”连接在一起的:

java,scala,go,python

进入源码一探究竟:

public static Collector<CharSequence, ?, String> joining(CharSequence delimiter) {
    return joining(delimiter, "", "");
}

可以看到它本身又调用了重载方法,所以我们将它与第三个重载方法一起研究。

  • 带分隔符和前缀后缀的重载方法
String result = Stream.of("java", "scala", "go", "python")
        .collect(Collectors.joining(",", "[", "]"));
System.out.println(result);

这个例子的结果如下,除了元素间利用分隔符连接,在字符串首位分别加了一个前后缀:

[java,scala,go,python]

Collectors#joining():

public static Collector<CharSequence, ?, String> joining(CharSequence delimiter,
                                                         CharSequence prefix,
                                                         CharSequence suffix) {
    return new CollectorImpl<>(
            () -> new StringJoiner(delimiter, prefix, suffix),
            StringJoiner::add, StringJoiner::merge,
            StringJoiner::toString, CH_NOID);
}

与不带参数的joining方法的区别是:这里使用StringJoiner来拼接字符串。StringJoiner是Java8提供的API,专门用于处理字符串的工具,内部使用的还是StringBuilder.

转换操作

mapping()方法

mapping()方法,接收两个参数:Function类型的函数mapper,用于转换每一个输入的元素;downStream,一个用于收集元素经过mapper函数转换过的结果的Collector。

示例:

List<Integer> result = Stream.of("java", "scala", "go", "python")
        .collect(Collectors.mapping(String::length, Collectors.toList()));
System.out.println(result);

这个示例是将Stream中的每一个元素转换成表示字符长度的整数,然后使用List收集到集合中。

[4, 5, 2, 6]

Collectors#mapping()源码:

public static <T, U, A, R>
Collector<T, ?, R> mapping(Function<? super T, ? extends U> mapper,
                           Collector<? super U, A, R> downstream) {
    BiConsumer<A, ? super U> downstreamAccumulator = downstream.accumulator();
    return new CollectorImpl<>(downstream.supplier(),
                                //重点在accumulator函数
                               (r, t) -> downstreamAccumulator.accept(r, mapper.apply(t)),
                               downstream.combiner(), downstream.finisher(),
                               downstream.characteristics());
}

mapping()方法返回一个新的Collector,这个Collector使用参数downStream提供的函数作为其函数,跟downStream唯一不同的是accumulator函数:利用mapper转换元素类型之后,再调用downStream的accumulator函数将结果并入到可变容器中。

收集操作

收集操作是指将Stream中的元素放入到一个指定的集合中,并返回这个集合。

toCollection()方法

toCollection()方法,使用一个指定的Collection实例收集元素。

示例:

List<String> result = Stream.of("java", "scala", "go", "python")
        .collect(Collectors.toCollection(LinkedList::new));
System.out.println(result);

示例中使用一个LinkedList收集Stream中的元素。

[java, scala, go, python]

Collectors#toCollection():

public static <T, C extends Collection<T>>
Collector<T, ?, C> toCollection(Supplier<C> collectionFactory) {
    return new CollectorImpl<>(collectionFactory, Collection<T>::add,
                               (r1, r2) -> { r1.addAll(r2); return r1; },
                               CH_ID);
}

toCollection()方法声明了一个collectionFactory参数,也就是说调用方可以指定收集元素的容器,返回的Collector利用collectionFactory作为创建可变容器的supplier函数,然后调用Collection#add()方法将元素添加到集合中。

toList()方法

toList()方法,返回的Collector使用一个ArrayList收集元素。这个Collector在开发当中使用频率非常高,我们直接看源码。

public static <T>
Collector<T, ?, List<T>> toList() {
    return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
                               (left, right) -> { left.addAll(right); return left; },
                               CH_ID);
}

可以看到与toCollection()非常相似,唯一的不同是使用方法引用ArrayList::new作为supplier函数。

toSet()方法

toSet()方法,返回的Collector使用一个HashSet收集元素。

public static <T>
Collector<T, ?, Set<T>> toSet() {
    return new CollectorImpl<>((Supplier<Set<T>>) HashSet::new, Set::add,
                               (left, right) -> { left.addAll(right); return left; },
                               CH_UNORDERED_ID);
}

与上面分析的toCollection()和toList()方法类似,不再展开。

toMap()方法

toMap()方法,返回一个Collector,这个Collector使用提供的Map容器收集元素结果,Map的key和value分别是参数keyMapper和valueMapper函数转换元素之后的结果。如果key重复,则会使用参数mergeFunction函数来合并value。

先来看一个示例:

Map<String, Integer> result = Stream.of("java", "scala", "go", "python")
        .collect(Collectors.toMap(item -> item, String::length));
System.out.println(result);

示例中返回一个Map,以元素本身作为key,以它的字符长度作为value。

{python=6, java=4, scala=5, go=2}

toMap()有三个重载方法,它们的区别在于参数不同,由于前两个方法都会调用第三个重载方法,所以我们先来看下它的实现原理:

public static <T, K, U, M extends Map<K, U>>
Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
                            Function<? super T, ? extends U> valueMapper,
                            BinaryOperator<U> mergeFunction,
                            Supplier<M> mapSupplier) {
    //重点关注这里
    BiConsumer<M, T> accumulator
            = (map, element) -> map.merge(keyMapper.apply(element),
                                          valueMapper.apply(element), mergeFunction);
    return new CollectorImpl<>(mapSupplier, accumulator, mapMerger(mergeFunction), CH_ID);
}

toMap()方法使用mapSupplier函数创建Map容器存放收集结果。每一个元素,都会经过keyMapper和valueMapper函数调用生成key和value,并将结果放入到Map容器中。注意这里调用的是Map#merge()方法,也就是说如果map中key对应的value已经存在且不为null,则会交给mergeFunction函数决断,这个函数接收oldValue和当前的value,返回一个新的value。

另外两个重载方法都会调用这个方法,我们来看一下:

  • 重载方法一
public static <T, K, U>
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                Function<? super T, ? extends U> valueMapper) {
    return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
}

private static <T> BinaryOperator<T> throwingMerger() {
    return (u,v) -> { throw new IllegalStateException(String.format("Duplicate key %s", u)); };
}

这个方法只需要提供keyMapper和valueMapper,使用HashMap收集结果,如果key重复则抛出异常IllegalStateException。

  • 重载方法二
public static <T, K, U>
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                Function<? super T, ? extends U> valueMapper,
                                BinaryOperator<U> mergeFunction) {
    return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new);
}

同样使用HashMap收集结果,但是对于key重复的情况,交给调用方处理,更加灵活一些。

toConcurrentMap()方法

toConcurrentMap()方法依然有三个重载方法,与toMap()一样使用Map收集结果。只是通过泛型限定了必须为 ConcurrentMap类型,所以这类方法返回的Collector在并发流中是线程安全的。

//重载方法一
public static <T, K, U>
Collector<T, ?, ConcurrentMap<K,U>> toConcurrentMap(Function<? super T, ? extends K> keyMapper,
                                                    Function<? super T, ? extends U> valueMapper) {
    return toConcurrentMap(keyMapper, valueMapper, throwingMerger(), ConcurrentHashMap::new);
}

//重载方法二
public static <T, K, U>
Collector<T, ?, ConcurrentMap<K,U>>
toConcurrentMap(Function<? super T, ? extends K> keyMapper,
                Function<? super T, ? extends U> valueMapper,
                BinaryOperator<U> mergeFunction) {
    return toConcurrentMap(keyMapper, valueMapper, mergeFunction, ConcurrentHashMap::new);
}

//重载方法三
public static <T, K, U, M extends ConcurrentMap<K, U>>
Collector<T, ?, M> toConcurrentMap(Function<? super T, ? extends K> keyMapper,
                                   Function<? super T, ? extends U> valueMapper,
                                   BinaryOperator<U> mergeFunction,
                                   Supplier<M> mapSupplier) {
    BiConsumer<M, T> accumulator
            = (map, element) -> map.merge(keyMapper.apply(element),
                                          valueMapper.apply(element), mergeFunction);
    return new CollectorImpl<>(mapSupplier, accumulator, mapMerger(mergeFunction), CH_CONCURRENT_ID);
}

分组分区

Collectors提供三类方法用于分组分区,分别是groupBy()、groupByConcurrent()和partionBy()。groupBy()在开发中尤其常用,由于本系列文章主要讲解串行流,所以不会分析与并发相关的groupByConcurrent()方法。

groupBy()方法

groupBy()方法返回一个Collector,这个Collector使用一个分组函数对元素进行分组,然后对于分组之后的元素,使用downStream收集。

示例:

Map<Integer, List<String>> result = Stream.of("java", "scala", "go", "python")
        .collect(Collectors.groupingBy(String::length));
System.out.println(result);

结果:

{2=[go], 4=[java], 5=[scala], 6=[python]}

上面的示例中,利用元素的字符长度对元素分组,然后将分组之后的元素收集到List集合中,最终以Map的形式表示分组结果。

groupBy()有三个重载方法,先分析最复杂的那个:

public static <T, K, D, A, M extends Map<K, D>>
Collector<T, ?, M> groupingBy(Function<? super T, ? extends K> classifier,
                              Supplier<M> mapFactory,
                              Collector<? super T, A, D> downstream) {
    //downstream的supplier函数,用于创建存放分组之后每一组元素的容器
    Supplier<A> downstreamSupplier = downstream.supplier();
    //downstream的accumulator函数
    BiConsumer<A, ? super T> downstreamAccumulator = downstream.accumulator();
    //这是返回的Collector的accumulator函数,重点关注
    BiConsumer<Map<K, A>, T> accumulator = (m, t) -> {
        //对于每一个元素,调用分组函数classifier计算分组key
        K key = Objects.requireNonNull(classifier.apply(t), "element cannot be mapped to a null key");
        //从map容器中获取分组key对应的存放单组元素的容器,
        //则调用downStream的supplier函数创建一个,并且放入map容器
        A container = m.computeIfAbsent(key, k -> downstreamSupplier.get()); 
        //调用downStream的accumulator函数,将元素并入到所属组的容器中
        downstreamAccumulator.accept(container, t);
    };
    BinaryOperator<Map<K, A>> merger = Collectors.<K, A, Map<K, A>>mapMerger(downstream.combiner());
    @SuppressWarnings("unchecked")
    Supplier<Map<K, A>> mangledFactory = (Supplier<Map<K, A>>) mapFactory;

    if (downstream.characteristics().contains(Collector.Characteristics.IDENTITY_FINISH)) {
        //downStream的特征包含IDENTITY_FINISH,说明分组之后存放每组元素的容器不需要转换
        return new CollectorImpl<>(mangledFactory, accumulator, merger, CH_ID);
    }
    else {
        @SuppressWarnings("unchecked")
        Function<A, A> downstreamFinisher = (Function<A, A>) downstream.finisher(); 
        //downStream的特征不包含IDENTITY_FINISH,需要声明一个finisher函数,这个函数内部会应用downStream的finisher函数转换存放每组元素的容器
        Function<Map<K, A>, M> finisher = intermediate -> {
            intermediate.replaceAll((k, v) -> downstreamFinisher.apply(v));
            @SuppressWarnings("unchecked")
            M castResult = (M) intermediate;
            return castResult;
        };
        return new CollectorImpl<>(mangledFactory, accumulator, merger, finisher, CH_NOID);
    }
}

这个方法十分复杂,需要传入三个参数:

  • classifier:用于对元素分组的函数。
  • mapFactory:用于创建存放分组结果的容器的函数,调用它将返回一个Map。
  • downStream:对分组之后每一组的元素进行收集的Collector。

根据上面的源码和注释,我们可以重构一下对应终止操作的处理流程:

  1. 在遍历元素之前,调用Sink#begin()方法,在内部会调用mapFactory函数创建一个map容器,并保存到变量state上,这个map用于存放分组结果。
  2. 开始遍历元素,每一个元素都会调用Sink#accept()方法,这个方法间接调用collector的accumulator函数。
  3. 在accumulator函数中,首先会调用分组函数classifier计算元素在map容器中的key;然后根据这个key在map中查找元素所属的单组元素的容器,如果找不到则调用downStream的supplier函数创建,并保存到map中;最后调用downStream的accumulator函数将元素并入到所属组的容器中。
  4. 重复步骤2、3,直到结束元素遍历。
  5. 如果Collector包含IDENTITY_FINISH特征,则map作为最终结果返回;否则对map中的每一个值,也就是单组容器,调用downStream的finisher函数转换之后再返回。

分析完这个方法之后,两个重载方法就很容易理解,它们都会直接或间接调用这个方法:

  • 重载方法一
public static <T, K, A, D>
Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier,
                                      Collector<? super T, A, D> downstream) {
    return groupingBy(classifier, HashMap::new, downstream);
}

这个方法写死了mapFactory函数,创建的是HashMap。

  • 重载方法二
public static <T, K> Collector<T, ?, Map<K, List<T>>>
groupingBy(Function<? super T, ? extends K> classifier) {
    return groupingBy(classifier, toList());
}

这个方法以Collectors#toList()返回的Collector作为downStream参数,调用重载方法一。

partionBy()方法

partionBy()方法相对groupBy()方法没有那么常用,我们先来看一下如何使用:

Map<Boolean, List<String>> result = Stream.of("java", "scala", "go", "python")
        .collect(Collectors.partitioningBy(item -> item.length() > 4));
System.out.println(result);

partionBy()返回的Collector同样以Map容器来对元素分组,不过限定了key为Boolean类型。在这个例子中,依据元素的长度是否大于4来分组,下面是输出结果:

{false=[java, go], true=[scala, python]}

partionBy()方法内部同groupBy()一样复杂,不过原理是一样的,有了上面的基础之后,更容易理解:

public static <T, D, A>
Collector<T, ?, Map<Boolean, D>> partitioningBy(Predicate<? super T> predicate,
                                                Collector<? super T, A, D> downstream) {
    BiConsumer<A, ? super T> downstreamAccumulator = downstream.accumulator();
    //这里是重点,利用predicate计算元素所属true组或false组,然后调用downStream的accumulator函数收集元素到所属组中
    BiConsumer<Partition<A>, T> accumulator = (result, t) ->
            downstreamAccumulator.accept(predicate.test(t) ? result.forTrue : result.forFalse, t);
    BinaryOperator<A> op = downstream.combiner();
    BinaryOperator<Partition<A>> merger = (left, right) ->
            new Partition<>(op.apply(left.forTrue, right.forTrue),
                            op.apply(left.forFalse, right.forFalse));
    //supplier函数创建的是Partition对象,Partition实现了Map接口,是Collectors的内部类
    Supplier<Partition<A>> supplier = () ->
            new Partition<>(downstream.supplier().get(),
                            downstream.supplier().get());
    if (downstream.characteristics().contains(Collector.Characteristics.IDENTITY_FINISH)) {
        return new CollectorImpl<>(supplier, accumulator, merger, CH_ID);
    }
    else {
        Function<Partition<A>, Map<Boolean, D>> finisher = par ->
                new Partition<>(downstream.finisher().apply(par.forTrue),
                                downstream.finisher().apply(par.forFalse));
        return new CollectorImpl<>(supplier, accumulator, merger, finisher, CH_NOID);
    }
}

可以看到与groupBy()方法的逻辑基本一样,不过需要注意的是这里用于保存分组结果的是Collectors的内部类Partition,它实现了Map接口,感兴趣的小伙伴可以自己看一下。

折叠操作

折叠操作方法主要有reducing()、maxBy()、minBy()和counting(),其中maxBy()、minBy()和counting()方法都是依靠reducing()的。

reducing()方法

reducing()方法的作用跟Stream#Reduce()方法是一样的,不过又使用Collector实现了一遍。Collectors中有三个reducing()重载方法,下面分别对它们进行讲解:

  • 重载方法一
public static <T> Collector<T, ?, Optional<T>>
reducing(BinaryOperator<T> op) {
    class OptionalBox implements Consumer<T> {
        //使用value保存结果,present标记是否存在结果
        T value = null;
        boolean present = false;

        @Override
        public void accept(T t) {
            if (present) {
                value = op.apply(value, t);
            }
            else {
                value = t;
                present = true;
            }
        }
    }

    return new CollectorImpl<T, OptionalBox, Optional<T>>(
            OptionalBox::new, OptionalBox::accept,
            (a, b) -> { if (b.present) a.accept(b.value); return a; },
            a -> Optional.ofNullable(a.value), CH_NOID);
}

可以看到它跟Stream#reduce()方法的内部类ReducingSink逻辑是一样的,这里Collector的supplier函数创建的是OptionalBox对象,OptionalBox作为存放结果的容器,使用value保存结果,present标记是否存在结果,注意这个Collector返回的结果仍然是一个包含最终结果的Optional。

  • 重载方法二
public static <T> Collector<T, ?, T>
reducing(T identity, BinaryOperator<T> op) {
    return new CollectorImpl<>(
            boxSupplier(identity),
            (a, t) -> { a[0] = op.apply(a[0], t); },
            (a, b) -> { a[0] = op.apply(a[0], b[0]); return a; },
            a -> a[0],
            CH_NOID);
}

@SuppressWarnings("unchecked")
private static <T> Supplier<T[]> boxSupplier(T identity) {
    return () -> (T[]) new Object[] { identity };
}

使用一个数组保存结果,这个数组下标为0的位置保存初始值identity,在元素参与计算产生新的结果之后会覆盖数组的这个位置位置。

  • 重载方法三
public static <T, U>
Collector<T, ?, U> reducing(U identity,
                            Function<? super T, ? extends U> mapper,
                            BinaryOperator<U> op) {
    return new CollectorImpl<>(
            boxSupplier(identity),
            (a, t) -> { a[0] = op.apply(a[0], mapper.apply(t)); },
            (a, b) -> { a[0] = op.apply(a[0], b[0]); return a; },
            a -> a[0], CH_NOID);
}

跟重载方法二一样,使用数组保存结果,唯一不同的是:每一个元素都要经过mapper函数转换之后再参与计算。

maxBy()方法

maxBy()方法,取最大的元素作为结果,直接调用reducing()方法。

public static <T> Collector<T, ?, Optional<T>>
maxBy(Comparator<? super T> comparator) {
    return reducing(BinaryOperator.maxBy(comparator));
}

BinaryOperator.maxBy()方法:

public static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator) {
    Objects.requireNonNull(comparator);
    return (a, b) -> comparator.compare(a, b) >= 0 ? a : b;
}

minBy()方法

minBy()方法,取最小的元素作为结果返回,跟maxBy()一样,不再讲解。

public static <T> Collector<T, ?, Optional<T>>
minBy(Comparator<? super T> comparator) {
    return reducing(BinaryOperator.minBy(comparator));
}

counting()方法

counting()方法,计算Stream中元素的大小。也是对reducing()方法的应用。

public static <T> Collector<T, ?, Long>
counting() {
    return reducing(0L, e -> 1L, Long::sum);
}

数值计算

数值计算的操作主要有averagingXXX()、summingXXX()和summarizingXXX()三类,作用是将元素转换为数字之后,再做求和、求平均值等操作。下面以Int为例进行讲解。

averagingInt()方法

averagingInt()方法返回一个Collector,这个Collector将Stream中的元素转换为Integer类型之后,再求算术平均值,如果没有元素则返回0作为结果。

public static <T> Collector<T, ?, Double>
averagingInt(ToIntFunction<? super T> mapper) {
    return new CollectorImpl<>(
            () -> new long[2],
            (a, t) -> { a[0] += mapper.applyAsInt(t); a[1]++; },
            (a, b) -> { a[0] += b[0]; a[1] += b[1]; return a; },
            a -> (a[1] == 0) ? 0.0d : (double) a[0] / a[1], CH_NOID);
}

这个方法返回的Collector使用一个长度为2的long数组作为容器保存临时结果,下标为0的位置保存求和的值,下标为1的位置保存元素个数。每遍历一个元素,就调用mapper函数将元素转换成long值,与前面的和值相加并保存,记录的元素个数加1。遍历完所有元素之后,调用finisher函数求平均值。

summingInt()方法

summingInt()方法返回的Collector将元素转换为Integer整数并求和,如果没有元素则返回0。

public static <T> Collector<T, ?, Integer>
summingInt(ToIntFunction<? super T> mapper) {
    return new CollectorImpl<>(
            () -> new int[1],
            (a, t) -> { a[0] += mapper.applyAsInt(t); },
            (a, b) -> { a[0] += b[0]; return a; },
            a -> a[0], CH_NOID);
}

比averagingInt()方法更简单,是使用长度为1的int数组保存求和结果。

summarizingInt()方法

summarizingInt()方法返回一个Collector,这个Collector将Stream中的元素转换成Integer类型之后,再计算统计信息,返回一个IntSummaryStatistics对象。这个IntSummaryStatistics具有获取元素个数、和、最大值、最小值和平均值的能力。

public static <T>
Collector<T, ?, IntSummaryStatistics> summarizingInt(ToIntFunction<? super T> mapper) {
    return new CollectorImpl<T, IntSummaryStatistics, IntSummaryStatistics>( 
            IntSummaryStatistics::new,
            (r, t) -> r.accept(mapper.applyAsInt(t)),
            (l, r) -> { l.combine(r); return l; }, CH_ID);
}

这个方法中的Collector以IntSummaryStatistics作为容器保存结果,我们看一下它的源码:

public class IntSummaryStatistics implements IntConsumer {
    private long count;
    private long sum;
    private int min = Integer.MAX_VALUE;
    private int max = Integer.MIN_VALUE;

    public IntSummaryStatistics() { }

    @Override
    public void accept(int value) {
        ++count;
        sum += value;
        min = Math.min(min, value);
        max = Math.max(max, value);
    }

    public final long getCount() {
        return count;
    }

    public final long getSum() {
        return sum;
    }

    public final int getMin() {
        return min;
    }

    public final int getMax() {
        return max;
    }

    public final double getAverage() {
        return getCount() > 0 ? (double) getSum() / getCount() : 0.0d;
    }
}

上面是经过稍微精简之后的代码,计算逻辑在accept()方法中,很容易理解,就不再详细讲解。

使用技巧

到这里,Collectors中具备各种能力的工厂方法就讲解完了。有时候开发过程中,对数据的处理非常的复杂,可能有人会遇到Collectors中提供的方法不能满足自己需求的情况。不知是否还记得,上面分析各个方法源码时有看到某些方法的参数是名称为downStream的Collector,downStream意思是下游的收集器,可以利用这些方法灵活的组合各种Collector,完成自己的数据处理工作。

比如有这样的一个要求:
一批数据由java、java、scala、go、go、go和python几个元素组成。现在要对这些元素分组,然后求每组中元素的个数。

如果单单使用Collector是比较难满足需求的,但是使用downStream组合两个或者多个Collector可以很容易解决这个问题。

Map<String, Long> result = Stream.of("java", "java", "scala", "go", "go", "go", "python")
        .collect(Collectors.groupingBy(item -> item, Collectors.counting()));

运行一下,验证结果正确:

{python=1, java=2, scala=1, go=3}

自定义Collector

灵活运用Collectors中的方法基本上能够解决我们绝大多数问题,但在某些极特殊情况下Collectors起不到作用。这个时候有两种办法:第一种是老老实实地遍历元素,以命令式编写我们的代码逻辑;还有一种方法,既然Collectors不能满足我们的需求,我们又理解了Stream API的原理,不如自己定义Collector,这样除了满足临时的需求,可能还可以复用到其它地方,甚至可以作为一个通用的组件给其他同事使用,显然这种方法更优雅。

下面以一个例子来说明如何定义满足自己业务需求的Collector,假设有java、scala、go和python这几编程语言名称作为元素组成一批数据,现在要对这些数据经过Stream处理之后返回一个业务定义的实体对象LanguageCounts。

public class LanguageCounts {

    //字符长度大于4的元素个数
    private int gtFourLength;

    //字符长度小于等于4的元素个数
    private int leFourLength;

    public LanguageCounts(int gtFourLength, int leFourLength) {
        this.gtFourLength = gtFourLength;
        this.leFourLength = leFourLength;
    }

    public void setGtFourLength(int gtFourLength) {
        this.gtFourLength = gtFourLength;
    }

    public void setLeFourLength(int leFourLength) {
        this.leFourLength = leFourLength;
    }

    public int getGtFourLength() {
        return gtFourLength;
    }

    public int getLeFourLength() {
        return leFourLength;
    }

    @Override
    public String toString() {
        return "LanguageCounts{" +
                "gtFourLength=" + gtFourLength +
                ", leFourLength=" + leFourLength +
                '}';
    }
}

LanguageCounts定义了两个属性:gtFourLength表示字符长度大于4的元素个数;leFourLength表示字符长度小于等于4的元素个数。

我们只需要定义一个实现Collector的类,像填空一样编写它的方法就行了。

public class LanguageCountsCollector implements Collector<String, LanguageCounts, LanguageCounts> {

    @Override
    public Supplier<LanguageCounts> supplier() {
        return () -> new LanguageCounts(0, 0);
    }

    @Override
    public BiConsumer<LanguageCounts, String> accumulator() {
        return (languageCounts, language) -> {
            if (language.length() > 4) {
                languageCounts.setGtFourLength(languageCounts.getGtFourLength() + 1);
            } else {
                languageCounts.setLeFourLength(languageCounts.getLeFourLength() + 1);
            }
        };
    }

    @Override
    public BinaryOperator<LanguageCounts> combiner() {
        return (left, right) -> {
            left.setGtFourLength(left.getGtFourLength() + right.getGtFourLength());
            left.setLeFourLength(left.getLeFourLength() + right.getLeFourLength());
            return left;
        };
    }

    @Override
    public Function<LanguageCounts, LanguageCounts> finisher() {
        return languageCounts -> languageCounts;
    }

    @Override
    public Set<Characteristics> characteristics() {
        return EnumSet.of(Collector.Characteristics.IDENTITY_FINISH);
    }
}

定了好Collector之后,在编写数据处理逻辑时创建这个Collector对象。

LanguageCounts counts = Stream.of("java", "scala", "go", "python")
        .collect(new LanguageCountsCollector());
System.out.println(counts);

输出的结果完美符合要求:

LanguageCounts{gtFourLength=2, leFourLength=2}

总结

在前面几章,详细分析了Stream的创建、中间操作和终止操作原理。本章通过分与Collector相关的源码,讲解了Collector接口的定义,以及它与ReduceOp协同工作的原理。根据功能不同将Collectors工厂方法划分为6大类,然后一一讲解了它们的作用和实现原理。最后通过两个例子,介绍了通过组合Collecters方法灵活地创建更复杂的收集器,以及如何去实现Collector接口来满足我们的开发需求。

写在最后

  • 上面举的两个例子不一定非常恰当,主要是为了起到说明作用,能够让大家理解用意,达到举一反三的效果就好。 最后,原创不易,如果觉得本系列文章对您有帮助,能够加深您对Stream原理和源码的理解的话,请不要吝啬您手中的赞(✪ω✪)!