java8函数式编程--收集器collector

原创
2015/08/04 01:13
阅读数 10K

      java8的stream api能很方便我们对数据进行统计分类等工作,以前我们写的很多统计数据的代码往往是循环迭代得到的,不说别人看不懂,自己的代码放久了也要重新看一段时间才能看得懂。现在,java8吸收了适合科学计算的语言的新特性,提供了stream api,让我们方便并且直观地编写统计代码成为可能。

      stream里有一个collect(Collector c)方法,需要传入Collector收集器这个接口。现在就说说这个接口定义的职责。

public interface Collector<T, A, R> { 
    Supplier<A> supplier();
    BiConsumer<A, T> accumulator();
    BinaryOperator<A> combiner();
    Function<A, R> finisher();

    Set<Characteristics> characteristics();
}

Collector主要定义了容器的类型,添加元素的方法,容器合并的方法还有输出的结果。


supplier就是生成容器,accumulator是添加元素,combiner是合并容器,finisher是输出的结果,characteristics是定义容器的三个属性,包括是否有明确的finisher,是否需要同步,是否有序。


例如:Collectors里面有个tolist()方法,返回一个收集器如下

Collector(ArrayList::new,List::add,List::addAll,IDENTITY_FINISH)

      可以看到,先是一个ArrayList的实例化,然后是添加元素使用List的add方法,容器合并就是addAll方法。这里没有finisher,原因是最终结果要的就是List,finisher就是返回容器。但是stream.collect()方法如何得知?就是通过枚举类型得到的。三个枚举类型:

IDENTITY_FINISH   不用finisher,doc的描述为elided(可以省略的)
UNORDERED         集合是无序的
CONCURRENT        集合的操作需要同步

      定义好collector,最终传参进stream的collect方法里,这个终结的操作最后会通过你定义的统计和收集的操作进行收集。jdk源码中有一个Collectors类已经为我们定义好很多操作,我们只要简单的添加一些收集的定义就能为我们很好的工作了。

      这里重点讲一个groupingBy()方法。若果我们学过sql语句的话会了解到,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)

当你使用的是

groupingBy(e->e.getName())

其实调用了

groupingBy(e->e.getName(),toList())

而最后调用了

groupingBy(e->e.getName(),HashMap::new,toList())

也就是上面长长的一坨。究竟这个groupingBy方法给我们构造了一个怎样的Collector呢?现在我们分析一下。

以上面为例子,我们知道最后结果承载的容器是Map,更加准确的说,是一个HashMap。所以

supplier = HashMap::new

然后是accmulator,首先通过e->e.getName()获得Map的key,然后生成或取出key对应的value,然后对于toList()来说,value是List,然后将元素加进List中,可以得到

accmulator = (map,elemet)->
               {
                    1. 得到key,
                    2. 从map中通过key获得container,没有container的话实例化一个新的container
                    (通过downstream.supplier得到List),
                    3. 对container执行downstream.accmulator方法,也就是add方法
               }

上面的downstream就是toList方法给我们返回的collector,我们称之为下游收集器。

接着是combiner也使用了downstream.combiner,不过容器是map,得到

mapmerger(downstream.combiner)即是addAll

这里需要参考map的默认方法mapmerger方法。这个方法的大致意思是对map中每一个key进行遍历。

    private static <K, V, M extends Map<K,V>>
    BinaryOperator<M> mapMerger(BinaryOperator<V> mergeFunction) {
        return (m1, m2) -> {
            for (Map.Entry<K,V> e : m2.entrySet())
                m1.merge(e.getKey(), e.getValue(), mergeFunction);
            return m1;
        };
    }

最后我们就能得到这么一个收集器

Collector(supplier = HashMap::new,
          accmulator = { 得到key,得到container,使用下游的accmulator}
          combiner = mapmerger(downstream.combiner)
         )

finisher和characteristics不详细说明,因为一般来说finisher可以省略,当不能省略的时候,就是有下游收集器的finisher,源码里面有体会,需要理解清楚的可以认真源码。

再给一个例子:

collect(groupingBy(e->e.getArtist(),mapping(artist->artist.getName())))

最后得到的收集器

Collector(supplier = HashMap::new,
          accmulator = { 得到key,得到container,
                          使用下游的accmulator.accept(container,artist->artist.getName())
                       }
          combiner = mapmerger(downstream.combiner)
         )

--------------------------------------------------------------------------------------------------------------------------------------------------------------      这是快乐的分割线     ------------------------------------------------------------------------------------------------------------------------------------------------------------------


总结:

收集器功能强大,一般来说,jdk自带的Collectors里面的方法已经能满足一般需求,而了解Collectors的内部,让我们更加了解如何使用它。在写这篇博文的时候,我对collector的印象也加深了,所以建议大家也写一下blog,自己也会受益良多。不断学习新特性,有机会的话把它们加入到代码里。

希望这篇博文能让你有一丢丢收获!

展开阅读全文
打赏
2
6 收藏
分享
加载中
非常感谢楼主的博文,现在正在很有兴趣地研究这一块
2017/06/29 20:47
回复
举报
写的真好。赞,大多数都在想着怎么用,但是研究这些细节更能看出一个人的耐心和兴趣
2017/01/06 16:38
回复
举报
更多评论
打赏
2 评论
6 收藏
2
分享
返回顶部
顶部