【Java8新特性】Stream流收集器实战

在Stream API能够帮助我们简化集合数据的处理,在处理流时有两种操作

  • 中间操作

中间操作会返回另外一个流,这让多个操作可以连接起来,形成一个查询,中间操作调用之后并不会立即执行,会在执行终止操作时,一次性全部处理。例如filtersorted都属于中间操作

  • 终止操作

终止操作会从流的流水线生成结果。它的结果可以是任何不是流的值,例如List,Integer甚至是void。collect()就是其中一个终止操作。

collect()方法

collect()包含两个重载方法

方法一

collect()方法1如下所示,它的入参是3个函数式接口

java 复制代码
<R> R collect(Supplier<R> supplier,
                  BiConsumer<R, ? super T> accumulator,
                  BiConsumer<R, R> combiner);

它的三个入参分别为

  • Supplier<R> supplier

提供一个新的结果容器的supplier,如果是并行流,这个函数会被调用多次,因此每次都需要返回一个新的容器

  • BiConsumer<R, ? super T> accumulator

合并元素到结果容器的函数

  • BiConsumer<R, R> combiner

合并两个结果容器consumer

上面的collect()相当于下面这段代码

java 复制代码
R result = supplier.get();  
for (T element : this stream)      
  accumulator.accept(result, element);  
return result;

我们通过下面代码验证上面代码的执行情况

java 复制代码
public static void main(String[] args) {
    List<Integer> result = IntStream.range(1, 5)
            .collect(
            () -> {
                System.out.println("====> 创建容器");
                return new ArrayList<>();
            },
            ((arrayList, value) -> {
                System.out.println("====> 累加值:" + value + "\t累加结果数组:" + ListUtil.toString(arrayList));
                arrayList.add(value);
            }),
            (list1, list2) -> {
                System.out.println("<==== 结果合并:" + "结果1:" + ListUtil.toString(list1) + "\t结果2" + ListUtil.toString(list2));
                list1.addAll(list2);
            }
    );
    ListUtil.printArray(result, "----------> 输出结果");
}

执行结果如下所示

可以看到第三个consumer并没有被执行,在整个collect过程中,只创建了一个容器,然后将流中的数据添加到容器中,并不需要合并容器,将IntStream改成并行流

java 复制代码
List<Integer> result = IntStream.range(1, 5)
        .parallel()   // 改成并行流,其他不变
        .collect(
        () -> {
            System.out.println("====> 创建容器");
            return new ArrayList<>();
        },
        ((arrayList, value) -> {
            System.out.println("====> 累加值:" + value + "\t累加结果数组:" + ListUtil.toString(arrayList));
            arrayList.add(value);
        }),
        (list1, list2) -> {
            System.out.println("<==== 结果合并:" + "结果1:" + ListUtil.toString(list1) + "\t结果2" + ListUtil.toString(list2));
            list1.addAll(list2);
        }
);
ListUtil.printArray(result, "----------> 输出结果");

执行结果如下所示,在collect()过程创建了4个容器,执行了3次合并,将4个容器合并成最终结果容器并返回。

方法二

这个方法和上面的不同是入参只有一个,只需要传入一个Collector接口

java 复制代码
<R, A> R collect(Collector<? super T, A, R> collector);

Collector接口源码如下所示

java 复制代码
public interface Collector<T, A, R> {
  	// 提供新容器Supplier
    Supplier<A> supplier();
		// 元素合并Consumer
    BiConsumer<A, T> accumulator();
		// 容器合并conbiner
    BinaryOperator<A> combiner();
		// 类型转换finisher,可以将A转成R
    Function<A, R> finisher();
		// collect
    Set<Characteristics> characteristics();
		// 底层创建CollectorImpl类
    public static<T, R> Collector<T, R, R> of(Supplier<R> supplier,
                                              BiConsumer<R, T> accumulator,
                                              BinaryOperator<R> combiner,
                                              Characteristics... characteristics) {
				// ... 省略部分代码
        return new Collectors.CollectorImpl<>(supplier, accumulator, combiner, cs);
    }
		// 底层创建CollectorImpl类
    public static<T, A, R> Collector<T, A, R> of(Supplier<A> supplier,
                                                 BiConsumer<A, T> accumulator,
                                                 BinaryOperator<A> combiner,
                                                 Function<A, R> finisher,
                                                 Characteristics... characteristics) {
				// ... 省略部分代码
        return new Collectors.CollectorImpl<>(supplier, accumulator, combiner, finisher, cs);
    }
		// Collector特性
    enum Characteristics {
				// 并行流Collector标记
        CONCURRENT,
				// 集合操作是无序的
        UNORDERED,
				// 标识finisher,如果设置了,则从A到R的未检查强制转换必须成功。
        IDENTITY_FINISH
    }
}

我们将上面的代码通过Collector的方式改造,改造代码如下

java 复制代码
public static void main(String[] args) {
    // 自定义Collector
    Collector<Integer, List<Integer>, List<Integer>> collector = Collector.of(ArrayList::new	// 创建结果容器
            , List::add								// 将流中的对象添加到List容器中
            , (list1, list2) -> {
                list1.addAll(list2);
                return list1;
            }
    );
    // 通过自定义Collector收集result
    List<Integer> result = Stream.of(1, 2, 3, 4, 5)
            .collect(collector);
    // 打印collect结果
    ListUtil.printArray(result);
}

输出结果如下所示,方法一中的结果相同

Collectors详解

Collectors是Collector接口的不同实现,它包含多种终止操作,例如将元素收集到Collection中,将元素汇总。

下面截取了Collectors中的部分源码,Collectors的构造方法是私有的,因此我们在使用时不能直接构建Collectors。在Collectors并没有直接实现Collector接口,而是通过静态内部类CollectorImpl实现了Collector接口

java 复制代码
public final class Collectors {
  	// ...
		// 私有构造方法
    private Collectors() { }
		// Collector实现类
    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;
        }
        CollectorImpl(Supplier<A> supplier,
                      BiConsumer<A, T> accumulator,
                      BinaryOperator<A> combiner,
                      Set<Characteristics> characteristics) {
            this(supplier, accumulator, combiner, castingIdentity(), characteristics);
        }
        @Override
        public BiConsumer<A, T> accumulator() {
            return accumulator;
        }
        @Override
        public Supplier<A> supplier() {
            return supplier;
        }
        @Override
        public BinaryOperator<A> combiner() {
            return combiner;
        }
        @Override
        public Function<A, R> finisher() {
            return finisher;
        }
        @Override
        public Set<Characteristics> characteristics() {
            return characteristics;
        }
    }
  	// ...
}
将元素收集到容器(toCollection,toList,toSet)

Collectors提供了三种将流中的元素收集到容器中的方法

  • toCollection(Supplier<C> collectionFactory):将流中的元素收集到Collection中
  • toList:将流中的元素收集到List中
  • toSet:将流中的元素收集到Set中
java 复制代码
// java.util.stream.Collectors源码
// toCollection将元素收集到入参Supplier提供的Collection容器中
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);
}
// toList,将元素收集到ArrayList中
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);
}
// toSet,将元素收集到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);
}

使用代码演示

java 复制代码
// toCollection使用方法,如果supplier传入的是ArrayList::new,那么相当于是Collection.toList()
List<Integer> result1 = Stream.of(1, 2, 3, 4, 5).collect(Collectors.toCollection(ArrayList::new));
// toList使用方法
List<Integer> result2 = Stream.of(1, 2, 3, 4, 5).collect(Collectors.toList());
// toSet使用方法
Set<Integer> result3 = Stream.of(1, 2, 3, 4, 5).collect(Collectors.toSet());
将流中元素拼接成字符串(joining)

Collectors提供了三种将元素拼接成字符串的方法

  • joining():将流中的元素直接拼接成字符串
  • joining(CharSequence delimiter):将流中的元素拼接成字符串,并用delimiter分隔
  • joining(CharSequence delimiter,CharSequence prefix,CharSequence suffix):将流中的元素拼接成字符串,并支持前缀,后缀和分隔符
java 复制代码
// 利用StringBuilder进行拼接
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);
}
// 前后缀默认为空
public static Collector<CharSequence, ?, String> joining(CharSequence delimiter) {
    return joining(delimiter, "", "");
}
// 底层使用StringJoiner进行拼接
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);
}

Stringjoiner底层其实也是通过StringBuilder拼接

使用代码演示

java 复制代码
public static void main(String[] args) {
    // 直接join
    System.out.println(Stream.of("1", "2", "3", "4", "5").collect(Collectors.joining()));
    // 使用,分隔
    System.out.println(Stream.of("1", "2", "3", "4", "5").collect(Collectors.joining(",")));
    // [前缀,]后缀,,分隔
    System.out.println(Stream.of("1", "2", "3", "4", "5").collect(Collectors.joining(",","[","]")));
}

执行结果如下

将流中元素收集到Map中(toMap)
  • toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper)

这个toMap()方法包含两个参数,一个是流中的元素转换成map的key的mapper,另一个是将流中的元素转换成value的map

使用示例如下

java 复制代码
// 将Person流转换成key是age,value是name的map
Map<Integer, String> result = Stream.of(new Person(18, "linshifu"), new Person(17, "Xiaozhang"))
                .collect(Collectors.toMap(Person::getAge,Person::getName));

上面的toMap方法必须要保证流中的元素转成的key必须是唯一的,否则会抛出异常,例如

java 复制代码
// 与上面例子相似,流中有两个年龄
Map<Integer, String> result = Stream.of(new Person(18, "linshifu"), new Person(17, "Xiaozhang"),new Person(18, "XiaoLi"))
        .collect(Collectors.toMap(Person::getAge,Person::getName));

执行结果如下所示

如果想避免抛出异常,可以使用下面的方法

  • toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction)

相比上面的toMap(),这个方法中增加了BinaryOperator<U> mergeFunction提供了两个元素合并的参数,例如

java 复制代码
Map<Integer, String> result1 = Stream.of(new Person(18, "linshifu"),
                new Person(17, "Xiaozhang"), new Person(18, "Xiaoli"))
        .collect(Collectors.toMap(Person::getAge, Person::getName, (name1, name2) -> name1));
System.out.println("result1: "+result1);
// 如果key重复,则将name进行拼接,并用,分隔
Map<Integer, String> result2 = Stream.of(new Person(18, "linshifu"),
                new Person(17, "Xiaozhang"), new Person(18, "Xiaoli"))
        .collect(Collectors.toMap(Person::getAge, Person::getName, (name1, name2) -> name1+","+name2));
System.out.println("result2: "+result2);

执行过程没有报错,执行结果如下所示

上面的流转map默认是创建一个HashMap,如果我们想要自己提供Map容器,可以使用下面方法

  • toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction, Supplier<M> mapSupplier)

它增加了Supplier<M> mapSupplier入参,可以自己提供一个Map容器,例如

java 复制代码
// 使用LinkedHashMap作为收集容器
Map<Integer, String> result3 = Stream.of(new Person(18, "linshifu"),
                new Person(17, "Xiaozhang"), new Person(18, "Xiaoli"))
        .collect(Collectors.toMap(Person::getAge, Person::getName, (name1, name2) -> name1, LinkedHashMap::new));
System.out.println("result3: "+result3);
将流中元素分组(groupingBy)

Collctors中的groupingBy()可以实现分组操作,将流中的元素进行分组,分组后会得到Map<K,D>

入参:

  • Function<? super T, ? extends K> classifier:将元素分组Function
  • Supplier<M> mapFactory:产生返回Map的Supplier
  • Collector<? super T, A, D> downstream:聚合的Collector
java 复制代码
// java.util.stream.Collectors#groupingBy
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)

使用例子如下

java 复制代码
// 按照年龄分组,年龄作为key,将相同年龄的person收集到List中
Map<Integer, List<Person>> result1 = Stream.of(new Person(18, "linshifu"),
                new Person(17, "Xiaozhang"), new Person(18, "Xiaoli"))
        .collect(Collectors.groupingBy(Person::getAge, HashMap::new, Collectors.toList()));
System.out.println("result1:"+result1);
// 按照年龄分组,年龄作为key,将相同年龄的person收集到Set中
Map<Integer, Set<Person>> result2 = Stream.of(new Person(18, "linshifu"),
                new Person(17, "Xiaozhang"), new Person(18, "Xiaoli"))
        .collect(Collectors.groupingBy(Person::getAge, HashMap::new, Collectors.toSet()));
System.out.println("result2:"+result2);

执行结果如下

Collectors.groupingBy()与Collectors.toMap()对比

Collectors.toMap()适用于通过键(Map)收集到Value包含单个值

Collectors.groupingBy()适用于通过键(Map)收集到value包含多个值(List,Set)

Collectors还提供了另外两种groupingBy的重载方法

java 复制代码
// 默认收集到HashMap中,我们只需要传入分组函数(classifier)和收集器(downstream)
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);
}
// 默认收集到HashMap<K,List<V>>中,只需要传入分组函数(classifier)
public static <T, K> Collector<T, ?, Map<K, List<T>>>
groupingBy(Function<? super T, ? extends K> classifier) {
    return groupingBy(classifier, toList());
}
将流元素分区(partitionBy)

虽然在Collectors里的方法叫partitionBy,但是只能将流中的元素分到Key时Boolean的Map中,Collectors提供了两个分区函数

java 复制代码
public static <T>
Collector<T, ?, Map<Boolean, List<T>>> partitioningBy(Predicate<? super T> predicate)

入参是Predicate<? super T> predicate,用于判断将流中的元素划分到true还是false分区
*

java 复制代码
public static <T, D, A>
Collector<T, ?, Map<Boolean, D>> partitioningBy(Predicate<? super T> predicate,
                                                Collector<? super T, A, D> downstream)

这个方法相比上面的方法增加了聚合的Collector(Collector<? super T, A, D> downstream)

使用例子如下

java 复制代码
// 按照年龄分组,年龄作为key,将相同年龄的person收集到List中
Map<Boolean, List<Person>> result1 = Stream.of(new Person(18, "linshifu"),
                new Person(17, "Xiaozhang"), new Person(19, "Xiaoli"))
        .collect(Collectors.partitioningBy(p -> p.getAge() % 2 == 0));
System.out.println(result1);
// 将流中的元素按照年龄分区,分区中的结果输出到Set中
Map<Boolean, Set<Person>> result2 = Stream.of(new Person(18, "linshifu"),
                new Person(17, "Xiaozhang"), new Person(19, "Xiaoli"))
        .collect(Collectors.partitioningBy(p -> p.getAge() % 2 == 0, Collectors.toSet()));
System.out.println(result2);

执行结果如下

求平均值(averagingInt,averagingLong,averagingDouble)

Collectors提供了三个求平均值的实现方法

java 复制代码
public static <T> Collector<T, ?, Double>
averagingInt(ToIntFunction<? super T> mapper)

计算Int的平均值:入参为将流中的元素转为int的函数,返回Double

java 复制代码
public static <T> Collector<T, ?, Double>
averagingLong(ToLongFunction<? super T> mapper)

计算Long的平均值:入参为将流中的元素转为Long的函数,返回Double

java 复制代码
public static <T> Collector<T, ?, Double>
averagingDouble(ToDoubleFunction<? super T> mapper) 

计算Double的平均值:入参为将流中的元素转为Long的函数,返回Double

使用例子如下

java 复制代码
// 求流中所有人年龄的平均值
Double result = Stream.of(new Person(18, "linshifu"),
                new Person(17, "Xiaozhang"), new Person(19, "Xiaoli"))
        .collect(Collectors.averagingInt(Person::getAge));
System.out.println(result);
求统计值(summarizingInt,summarizingLong,summarizingDouble)

Collectors提供了三个求统计值的实现方法

java 复制代码
public static <T>
Collector<T, ?, IntSummaryStatistics> summarizingInt(ToIntFunction<? super T> mapper)

计算Int的统计值:入参为将流中的元素转为int的函数,返回IntSummaryStatistics

java 复制代码
public static <T>
Collector<T, ?, LongSummaryStatistics> summarizingLong(ToLongFunction<? super T> mapper)

计算Long的统计值:入参为将流中的元素转为Long的函数,返回LongSummaryStatistics

java 复制代码
public static <T>
Collector<T, ?, DoubleSummaryStatistics> summarizingDouble(ToDoubleFunction<? super T> mapper)

计算Double的统计值:入参为将流中的元素转为Double的函数,返回DoubleSummaryStatistics

IntSummaryStatistics,LongSummaryStatistics,DoubleSummaryStatistics包含数量统计(count),汇总值(sum),最小值(min),最大值(max),平均值(average)

使用例子如下

java 复制代码
// 求所有人年龄的统计值
IntSummaryStatistics result = Stream.of(new Person(18, "linshifu"),
                new Person(17, "Xiaozhang"), new Person(19, "Xiaoli"))
        .collect(Collectors.summarizingInt(Person::getAge));
System.out.println(result);

执行结果如下

求最大值、最小值(maxBy,minBy)

获取流中最小的元素

java 复制代码
public static <T> Collector<T, ?, Optional<T>>
minBy(Comparator<? super T> comparator)

获取流中最大的元素

java 复制代码
public static <T> Collector<T, ?, Optional<T>>
minBy(Comparator<? super T> comparator)

入参都是Comparator<? super T> comparator,返回值是Optional<T>

使用例子如下

java 复制代码
// 获取age最大的Person
Optional<Person> maxAgePerson = Stream.of(new Person(18, "linshifu"), new Person(17, "Xiaozhang"))
        .collect(Collectors.maxBy(Comparator.comparing(Person::getAge)));
// 上面collect等价于.max(Comparator.comparing(Person::getAge));
System.out.println(maxAgePerson);
// 获取age最小的Person
Optional<Person> minAgePerson = Stream.of(new Person(18, "linshifu"), new Person(17, "Xiaozhang"))
        .collect(Collectors.minBy(Comparator.comparing(Person::getAge)));
// 上面collect方法等价于.min(Comparator.comparing(Person::getAge));
System.out.println(minAgePerson);
归集到单个值(reducing)

Collectors提供的归集方法有下面三个

  • reducing(T identity, BinaryOperator<T> op)

第一个参数是归集初始值(identity),第二个参数是归集函数BinaryOperator

  • reducing(BinaryOperator<T> op)

这个方法不需要传入初始值,执行时会取流中的第一个值作为初始值,只需要传入BinaryOperator,由于没有给初始值,因此返回的值可能为空,它与上面方法的区别是会返回Optional<T>

  • reducing(U identity, Function<? super T, ? extends U> mapper,BinaryOperator<U> op)

这个方法相比第一个方法,增加了mapping函数作为第二个参数,将流中的每个元素映射成另一个元素

使用例子如下

java 复制代码
// 计算1-10之和
Integer result1 = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
        .collect(Collectors.reducing(/*indentity*/0,/*BinaryOperator*/(a, b) -> a + b));
Optional<Integer> result2 = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
        .collect(Collectors.reducing(/*BinaryOperator*/(a, b) -> a + b));

// 计算流中所有Person的年龄之和
Integer age = Stream.of(new Person(18, "linshifu"), new Person(17, "Xiaozhang"))
        .collect(Collectors.reducing(0, /*map年龄*/Person::getAge, (a, b) -> a + b));
收集完后再执行其他操作(collectingAndThen)

收集完之后再执行finisher函数

  • collectingAndThen(Collector<T,A,R> downstream,Function<R,RR> finisher)

使用例子如下

java 复制代码
// 取出流中年龄最小的两个Person,并返回List
List<Person> result = Stream.of(new Person(18, "linshifu"),
                new Person(17, "Xiaozhang"), new Person(19, "Xiaoli"))
        .collect(Collectors.collectingAndThen(Collectors.toList(), personList -> {
            personList.sort(Comparator.comparingInt(Person::getAge));
            return personList.subList(0, 2);
        }));
System.out.println(result);

执行结果如下

相关推荐
Yz98764 分钟前
Hadoop里面MapReduce的序列化与Java序列化比较
java·大数据·jvm·hadoop·分布式·mapreduce·big data
凯哥Java6 分钟前
优化批处理流程:自定义BatchProcessorUtils的设计与应用
java·数据库·mysql
njnu@liyong14 分钟前
AOP-前置原理-怎么判断和拦截?
java·aop·拦截
末央&19 分钟前
【C++】内存管理
java·开发语言·c++
心之语歌32 分钟前
设计模式 享元模式(Flyweight Pattern)
java·设计模式·享元模式
MTingle33 分钟前
【Java EE】文件IO
java·java-ee
coffee_baby36 分钟前
享元模式详解:解锁高效资源管理的终极武器
java·spring boot·mybatis·享元模式
爱学习的真真子43 分钟前
菜鸟也能轻松上手的Java环境配置方法
java·开发语言
曳渔1 小时前
Java-数据结构-二叉树-习题(三)  ̄へ ̄
java·开发语言·数据结构·算法·链表
shark-chili1 小时前
数据结构与算法-Trie树添加与搜索
java·数据结构·算法·leetcode