上一节学习了Java8中比较常用的内置collector的用法。接下来就来理解下collector的组成。

Collector定义

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

要开始使用Collector接口,我们先来看看toList的实现方法,这个在日常中使用最频繁的东西其实也简单。

Collector接口定义了5个函数

public interface Collector<T, A, R> {
Supplier<A> supplier();
BiConsumer<A, T> accumulator();
BinaryOperator<A> combiner();
Function<A, R> finisher();
Set<Characteristics> characteristics();
}
  1. T是流中要收集的对象的泛型
  2. A是累加器的类型,累加器是在收集过程中用于累积部分结果的对象。
  3. R是收集操作得到的对象(通常但不一定是集合)的类型。

对于toList, 我们收集的对象是T, 累加器是List, 最终收集的结果也是一个List,于是创建ToListCollector如下:

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

理解Collector几个函数

建立新的结果容器 supplier方法

supplier方法必须返回一个结果为空的Supplier,也就是一个无参数函数,在调用时,它会创建一个空的累加器实例,供数据收集过程使用。就个人通俗的理解来说,这个方法定义你如何收集数据,之所以提炼出来就是为了让你可以传lambda表达式来指定收集器。对于toList, 我们直接返回一个空list就好。

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

累加器执行累加的具体实现 accumulator方法

accumulator方法会返回执行归约操作的函数,该函数将返回void。当遍历到流中第n个元素时,这个函数就会执行。函数有两个参数,第一个参数是累计值,第二参数是第n个元素。累加值与元素n如何做运算就是accumulator做的事情了。比如toList, 累加值就是一个List,对于元素n,当然就是add。

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

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

当遍历完流之后,我们需要对结果做一个处理,返回一个我们想要的结果。这就是finisher方法所定义的事情。finisher方法必须返回在累积过程的最后要调用的一个函数,以便将累加器对象转换为整个集合操作的最终结果, 这个返回的函数在执行时,会有个参数,该参数就是累积值,会有一个返回值,返回值就是我们最终要返回的东西。对于toList, 我最后就只要拿到那个收集的List就好,所以直接返回List。

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

对于接收一个参数,返回一个value,我们可以想到Function函数,正如finisher()的返回值。对于这个返回参数本身的做法,Function有个静态方法

static <T> Function<T, T> identity() {
return t -> t;
}

可以用Function.identity()代替上述lambda表达式。

合并两个结果容器 combiner

上面看起来似乎已经可以工作了,这是针对顺序执行的情况。我们知道Stream天然支持并行,但并行却不是毫无代价的。想要并行首先就必然要把任务分段,然后才能并行执行,最后还要合并。虽然Stream底层对我们透明的执行了并行,但如何并行还是需要取决于我们自己。这就是combiner要做的事情。combiner方法会返回一个供归约操作使用的函数,它定义了对流的各个子部分并行处理时,各个字部分归约所得的累加器要如何合并。对于toList而言,Stream会把流自动的分成几个并行的部分,每个部分都执行上述的归约,汇集成一个List。当全部完成后再合并成一个List。

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

这样,就可以对流并行归约了。它会用到Java7引入的分支/合并框架和Spliterator抽象。大概如下所示,

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

characteristics方法

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

Characteristics是一个包含三个项目的枚举:

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

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

上面这段话说的有点绕口,大概是说像Set生成的stream是无序的,这时候toList就可以并行。而ArrayList这种队列一样的数据结构则生成有序的stream,不能并行。

使用

直接传给collect方法就好。

List<Dish> rs = dishes
.stream()
.collect(new ToListCollector<>());

我们这样费尽心思去创建一个toListCollector,一个是为了熟悉Collector接口的用法,一个是方便重用。当再遇到这样的需求的时候就可以直接用这个自定义的函数了,所以才有toList()这个静态方法。否则,其实collect提供了重载函数可以直接定义这几个函数。比如,可以这样实现toList

List<Dish> dishes = dishes
.stream()
.collect(
ArrayList::new, //supplier
List::add, //accumulator
List::addAll //combiner
);

这种方法虽然简单,但可读性较差,而且当再次遇到这个需求时还要重写一遍,复用性差。

关于性能

对于stream提供的几个收集器已经可以满足绝大部分开发需求了,reduce提供了各种自定义。但有时候还是需要自定义collector才能实现。文中举例还是质数枚举算法。之前我们通过遍历平方根之内的数字来求质数。这次提出要用得到的质数减少取模运算。然而,悲剧的是我本地测算的结果显示,这个而所谓的优化版反而比原来的慢100倍。不过,还是把这个自定义收集器列出来。值得铭记的是,这个收集器是有序的,所以不能并行,那个这个combiner方法可以不要的,最好返回UnsupportedOperationException来警示此收集器的非并行性。

测试见 https://github.com/Ryan-Miao/l4Java/blob/master/src/test/java/com/test/java/stream/collect/PrimeNumbersCollectorTest.java

public class PrimeNumbersCollector implements
Collector<Integer, Map<Boolean, List<Integer>>, Map<Boolean, List<Integer>>> { @Override
public Supplier<Map<Boolean, List<Integer>>> supplier() {
return () -> {
Map<Boolean, List<Integer>> map = new HashMap<>();
map.put(true, new ArrayList<>());
map.put(false, new ArrayList<>());
return map;
};
} @Override
public BiConsumer<Map<Boolean, List<Integer>>, Integer> accumulator() {
return (Map<Boolean, List<Integer>> acc, Integer candidate) -> {
acc.get(isPrime(acc.get(true), candidate)).add(candidate);
};
} /**
* 从质数列表里取出来,看看是不是candidate的约数.
*
* @param primes 质数列表
* @param candidate 判断值
* @return true -> 质数; false->非质数。
*/
private static Boolean isPrime(
List<Integer> primes,
Integer candidate) {
int candidateRoot = (int) Math.sqrt((double) candidate);
return primes.stream().filter(p -> p<=candidateRoot).noneMatch(i -> candidate % i == 0);
} @Override
public BinaryOperator<Map<Boolean, List<Integer>>> combiner() {
return (Map<Boolean, List<Integer>> map1, Map<Boolean, List<Integer>> map2) -> {
map1.get(true).addAll(map2.get(true));
map1.get(false).addAll(map2.get(false));
return map1;
};
} @Override
public Function<Map<Boolean, List<Integer>>, Map<Boolean, List<Integer>>> finisher() {
return Function.identity();
} @Override
public Set<Characteristics> characteristics() {
return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH));
}
}

参考

  • Java8 in Action

Java8-理解Colloctor的更多相关文章

  1. [三]java8 函数式编程Stream 概念深入理解 Stream 运行原理 Stream设计思路

    Stream的概念定义   官方文档是永远的圣经~     表格内容来自https://docs.oracle.com/javase/8/docs/api/   Package java.util.s ...

  2. 深入理解Java8中Stream的实现原理

    Stream Pipelines 前面我们已经学会如何使用Stream API,用起来真的很爽,但简洁的方法下面似乎隐藏着无尽的秘密,如此强大的API是如何实现的呢?比如Pipeline是怎么执行的, ...

  3. java8 Lambda 表达式和函数式接口快速理解

    前言 接上篇文章 java8 新特性 由于上篇过于庞大,使得重点不够清晰,本篇单独拿出 java8 的 Lambda 表达式和函数式接口说明. Lambda 表达式 lambda 表达式其实就是使用了 ...

  4. Java8 的一些新特性的学习理解

    近期在学习队列相关的一些知识,在学习过程中发现Iterable<T>接口中新增了两个新的方法,出于好奇,就想知道这是什么东东,干什么用的.俗话说:实践出真知,所以就有了以下反复的测试. 先 ...

  5. Java8之深入理解Lambda

    lambda表达式实战 从例子引出lambda 传递Runnable创建Thread java8之前 Thread thread=new Thread(new Runnable() { @Overri ...

  6. java8中stream的map和flatmap的理解

    转自https://blog.csdn.net/wynjauu/article/details/78741093 假如我们有这样一个需求给定单词列表["Hello","W ...

  7. java8中 map和flatmap的理解

    假如我们有这样一个需求给定单词列表["Hello","World"],你想要返回列表["H","e","l&q ...

  8. 关于Java8函数式编程你需要了解的几点

    函数式编程与面向对象的设计方法在思路和手段上都各有千秋,在这里,我将简要介绍一下函数式编程与面向对象相比的一些特点和差异. 函数作为一等公民 在理解函数作为一等公民这句话时,让我们先来看一下一种非常常 ...

  9. Java8之——简洁优雅的Lambda表达式

    Java8发布之后,Lambda表达式,Stream等等之类的字眼边慢慢出现在我们字眼.就像是Java7出现了之后,大家看到了“钻石语法”,看到了try-with-resource等等.面对这些新东西 ...

  10. java8中CAS的增强

    注:ifeve.com的同名文章为本人所发,此文在其基础做了些调整.转载请注明出处! 一.java8中CAS的增强 前些天,我偶然地将之前写的用来测试AtomicInteger和synchronize ...

随机推荐

  1. python 字符串和字典

    一.字符串操作 name = "my name is \t {name} and i am {year} years old" 1.首字母大写 print(name.capital ...

  2. Oracle数据库游标精解

    游标 定义:标识结果集中数据行的一种容器(CURSOR),游标允许应用程序对查询语句返回的行结果集中的每一行进行相同或不同的操作,而不是一次对整个结果集进行同一种操作.实际上是一种能从包括多条数据记录 ...

  3. Hadoop完全分布式安装教程

    一.软件版本 Hadoop版本号:hadoop-2.6.0.tar: VMWare版本号:VMware-workstation-full-11.0.0-2305329 Ubuntu版本号:ubuntu ...

  4. eclipse怎么停止building workspace

    Eclipse 一直不停 building workspace完美解决总结 一.产生这个问题的原因多种 1.自动升级 2.未正确关闭 3.maven下载lib挂起 等.. 二.解决总结 (1).解决方 ...

  5. 日推20单词 Day02

    1.distinguish v. 区别,辨别 2.tension n. 紧张,不安 3.sympathy n. 同情,慰问 4.admiration n. 羡慕 5.jealousy n. 嫉妒 6. ...

  6. intelj idea中JRebel激活

    1.下载激活软件 https://github.com/ilanyu/ReverseProxy/releases/tag/v1.0 我这边下载的是: 2.双击运行 3.idea中, 不出意外,应该就激 ...

  7. web开发 c/s结构 和 b/s结构

    web开发 c/s结构 和 b/s结构 c/s结构 --client/server 客户端/服务器机构 如qq b/s结构 -- browser/server 浏览器/服务器结构 如网站 mvc设计 ...

  8. PyQuery用法详解

    PyQuery是强大而又灵活的网页解析库,如果你觉得正则写起来太麻烦,如果你觉得BeautifulSoup语法太难记,如果你熟悉jQuery的语法 那么,PyQuery就是你绝佳的选择. 一.初始化方 ...

  9. Eureka服务注册中心

    Eureka服务注册中心 最近在研究Spring Cloud,发现其中的组件实在是太多了,真的是头大,只能一块一块看,像盲人摸象一样.要想很短时间内掌握Spring Cloud是不可能的,小编就学习一 ...

  10. 简单的sql调优(批处理)

    最近在写一个java的爬虫程序时,遇到了一个大量数据进行插入更新和大量数据循环查询的问题,所以查了一下一般的调优的方式,下面主要介绍我采取的调优措施. 一 .调优思路 先说说我采取方式的调优的思路,这 ...