本系列文章经补充和完善,已修订整理成书《Java编程的逻辑》(马俊昌著),由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买:京东自营连接 html
![]()
上节初步介绍了Java 8中的函数式数据处理,对于collect方法,咱们只是演示了其最基本的应用,它还有不少强大的功能,好比,能够分组统计汇总,实现相似数据库查询语言SQL中的group by功能。java
具体都有哪些功能?有什么用?如何使用?基本原理是什么?本节进行详细讨论,咱们先来进一步理解下collect方法。git
在上节中,过滤获得90分以上的学生列表,代码是这样的:github
List<Student> above90List = students.stream()
.filter(t->t.getScore()>90)
.collect(Collectors.toList());
复制代码
最后的collect调用看上去很神奇,它究竟是怎么把Stream转换为List<Student>
的呢?先看下collect方法的定义:数据库
<R, A> R collect(Collector<? super T, A, R> collector) 复制代码
它接受一个收集器collector做为参数,类型是Collector,这是一个接口,它的定义基本是:编程
public interface Collector<T, A, R> {
Supplier<A> supplier();
BiConsumer<A, T> accumulator();
BinaryOperator<A> combiner();
Function<A, R> finisher();
Set<Characteristics> characteristics();
}
复制代码
在顺序流中,collect方法与这些接口方法的交互大概是这样的:swift
//首先调用工厂方法supplier建立一个存放处理状态的容器container,类型为A
A container = collector.supplier().get();
//而后对流中的每个元素t,调用累加器accumulator,参数为累计状态container和当前元素t
for (T t : data)
collector.accumulator().accept(container, t);
//最后调用finisher对累计状态container进行可能的调整,类型转换(A转换为R),并返回结果
return collector.finisher().apply(container);
复制代码
combiner只在并行流中有用,用于合并部分结果。characteristics用于标示收集器的特征,Collector接口的调用者能够利用这些特征进行一些优化,Characteristics是一个枚举,有三个值:CONCURRENT, UNORDERED和IDENTITY_FINISH,它们的含义咱们后面经过例子简要说明,目前能够忽略。微信
Collectors.toList()具体是什么呢?看下代码:并发
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);
}
复制代码
它的实现类是CollectorImpl,这是Collectors内部的一个私有类,实现很简单,主要就是定义了两个构造方法,接受函数式参数并赋值给内部变量。对toList来讲:app
也就是说,collect(Collectors.toList())背后的伪代码以下所示:
List<T> container = new ArrayList<>();
for (T t : data)
container.add(t);
return container;
复制代码
与toList相似的容器收集器还有toSet, toCollection, toMap等,咱们来看下。
toSet的使用与toList相似,只是它能够排重,就不举例了。toList背后的容器是ArrayList,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);
}
复制代码
CH_UNORDERED_ID
是一个静态变量,它的特征有两个,一个是IDENTITY_FINISH,表示返回结果即为Supplier建立的HashSet,另外一个是UNORDERED,表示收集器不会保留顺序,这也容易理解,由于背后容器是HashSet。
toCollection是一个通用的容器收集器,能够用于任何Collection接口的实现类,它接受一个工厂方法Supplier做为参数,具体代码为:
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);
}
复制代码
好比,若是但愿排重但又但愿保留出现的顺序,可使用LinkedHashSet,Collector能够这么建立:
Collectors.toCollection(LinkedHashSet::new)
复制代码
toMap将元素流转换为一个Map,咱们知道,Map有键和值两部分,toMap至少须要两个函数参数,一个将元素转换为键,另外一个将元素转换为值,其基本定义为:
public static <T, K, U> Collector<T, ?, Map<K,U>> toMap(
Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper)
复制代码
返回结果为Map<K,U>
,keyMapper将元素转换为键,valueMapper将元素转换为值。好比,将学生流转换为学生名称和分数的Map,代码能够为:
Map<String,Double> nameScoreMap = students.stream().collect(
Collectors.toMap(Student::getName, Student::getScore));
复制代码
这里,Student::getName是keyMapper,Student::getScore是valueMapper。
实践中,常常须要将一个对象列表按主键转换为一个Map,以便之后按照主键进行快速查找,好比,假定Student的主键是id,但愿转换学生流为学生id和学生对象的Map,代码能够为:
Map<String, Student> byIdMap = students.stream().collect(
Collectors.toMap(Student::getId, t -> t));
复制代码
t->t是valueMapper,表示值就是元素自己,这个函数用的比较多,接口Function定义了一个静态函数identity表示它,也就是说,上面的代码能够替换为:
Map<String, Student> byIdMap = students.stream().collect(
Collectors.toMap(Student::getId, Function.identity()));
复制代码
上面的toMap假定元素的键不能重复,若是有重复的,会抛出异常,好比:
Map<String,Integer> strLenMap = Stream.of("abc","hello","abc").collect(
Collectors.toMap(Function.identity(), t->t.length()));
复制代码
但愿获得字符串与其长度的Map,但因为包含重复字符串"abc",程序会抛出异常。这种状况下,咱们但愿的是程序忽略后面重复出现的元素,这时,可使用另外一个toMap函数:
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)
复制代码
相比前面的toMap,它接受一个额外的参数mergeFunction,它用于处理冲突,在收集一个新元素时,若是新元素的键已经存在了,系统会将新元素的值与键对应的旧值一块儿传递给mergeFunction获得一个值,而后用这个值给键赋值。
对于前面字符串长度的例子,新值与旧值实际上是同样的,咱们能够用任意一个值,代码能够为:
Map<String,Integer> strLenMap = Stream.of("abc","hello","abc").collect(
Collectors.toMap(Function.identity(),
t->t.length(), (oldValue,value)->value));
复制代码
有时,咱们可能但愿合并新值与旧值,好比一个联系人列表,对于相同的联系人,咱们但愿合并电话号码,mergeFunction能够定义为:
BinaryOperator<String> mergeFunction = (oldPhone,phone)->oldPhone+","+phone;
复制代码
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)
复制代码
相比前面的toMap,多了一个mapSupplier,它是Map的工厂方法,对于前面两个toMap,其mapSupplier实际上是HashMap::new。咱们知道,HashMap是没有任何顺序的,若是但愿保持元素出现的顺序,能够替换为LinkedHashMap,若是但愿收集的结果排序,可使用TreeMap。
toMap主要用于顺序流,对于并发流,Collectors有专门的名称为toConcurrentMap的收集器,它内部使用ConcurrentHashMap,用法相似,具体咱们就不讨论了。
除了将元素流收集到容器中,另外一个常见的操做是收集为一个字符串。好比,获取全部的学生名称,用逗号链接起来,传统上,代码看上去像这样:
StringBuilder sb = new StringBuilder();
for(Student t : students){
if(sb.length()>0){
sb.append(",");
}
sb.append(t.getName());
}
return sb.toString();
复制代码
针对这种常见的需求,Collectors提供了joining收集器:
public static Collector<CharSequence, ?, String> joining()
public static Collector<CharSequence, ?, String> joining(CharSequence delimiter)
public static Collector<CharSequence, ?, String> joining(
CharSequence delimiter, CharSequence prefix, CharSequence suffix)
复制代码
第一个就是简单的把元素链接起来,第二个支持一个分隔符,第三个更为通用,能够给整个结果字符串加个前缀和后缀。好比:
String result = Stream.of("abc","老马","hello")
.collect(Collectors.joining(",", "[", "]"));
System.out.println(result); ```
输出为:
```java
[abc,老马,hello]
复制代码
joining的内部也利用了StringBuilder,好比,第一个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);
}
复制代码
supplier是StringBuilder::new,accumulator是StringBuilder::append,finisher是StringBuilder::toString,CH_NOID表示特征集为空。
分组相似于数据库查询语言SQL中的group by语句,它将元素流中的每一个元素分到一个组,能够针对分组再进行处理和收集,分组的功能比较强大,咱们逐步来讲明。
为便于举例,咱们先修改下学生类Student,增长一个字段grade,表示年级,改下构造方法:
public Student(String name, String grade, double score) {
this.name = name;
this.grade = grade;
this.score = score;
}
复制代码
示例学生列表students改成:
static List<Student> students = Arrays.asList(new Student[] {
new Student("zhangsan", "1", 91d),
new Student("lisi", "2", 89d),
new Student("wangwu", "1", 50d),
new Student("zhaoliu", "2", 78d),
new Student("sunqi", "1", 59d)});
复制代码
最基本的分组收集器为:
public static <T, K> Collector<T, ?, Map<K, List<T>>>
groupingBy(Function<? super T, ? extends K> classifier)
复制代码
参数是一个类型为Function的分组器classifier,它将类型为T的元素转换为类型为K的一个值,这个值表示分组值,全部分组值同样的元素会被归为同一个组,放到一个列表中,因此返回值类型是Map<K, List>。 好比,将学生流按照年级进行分组,代码为:
Map<String, List<Student>> groups = students.stream()
.collect(Collectors.groupingBy(Student::getGrade));
复制代码
学生会分为两组,第一组键为"1",分组学生包括"zhangsan", "wangwu"和"sunqi",第二组键为"2",分组学生包括"lisi", "zhaoliu"。
这段代码基本等同于以下代码:
Map<String, List<Student>> groups = new HashMap<>();
for (Student t : students) {
String key = t.getGrade();
List<Student> container = groups.get(key);
if (container == null) {
container = new ArrayList<>();
groups.put(key, container);
}
container.add(t);
}
System.out.println(groups);
复制代码
显然,使用groupingBy要简洁清晰的多,但它究竟是怎么实现的呢?
groupingBy的代码为:
public static <T, K> Collector<T, ?, Map<K, List<T>>>
groupingBy(Function<? super T, ? extends K> classifier) {
return groupingBy(classifier, toList());
}
复制代码
它调用了第二个groupingBy方法,传递了toList收集器,其代码为:
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);
}
复制代码
这个方法接受一个下游收集器downstream做为参数,而后传递给下面更通用的函数:
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)
复制代码
classifier仍是分组器,mapFactory是返回Map的工厂方法,默认是HashMap::new,downstream表示下游收集器,下游收集器负责收集同一个分组内元素的结果。
对最通用的groupingBy函数返回的收集器,其收集元素的基本过程和伪代码为:
//先建立一个存放结果的Map
Map map = mapFactory.get();
for (T t : data) {
// 对每个元素,先分组
K key = classifier.apply(t);
// 找存放分组结果的容器,若是没有,让下游收集器建立,并放到Map中
A container = map.get(key);
if (container == null) {
container = downstream.supplier().get();
map.put(key, container);
}
// 将元素交给下游收集器(即分组收集器)收集
downstream.accumulator().accept(container, t);
}
// 调用分组收集器的finisher方法,转换结果
for (Map.Entry entry : map.entrySet()) {
entry.setValue(downstream.finisher().apply(entry.getValue()));
}
return map;
复制代码
在最基本的groupingBy函数中,下游收集器是toList,但下游收集器还能够是其余收集器,甚至是groupingBy,以构成多级分组,下面咱们来看更多的示例。
将元素按必定标准分为多组,而后计算每组的个数,按必定标准找最大或最小元素,这是一个常见的需求,Collectors提供了一些对应的收集器,通常用做下游收集器,好比:
//计数
public static <T> Collector<T, ?, Long> counting()
//计算最大值
public static <T> Collector<T, ?, Optional<T>> maxBy(Comparator<? super T> comparator)
//计算最小值
public static <T> Collector<T, ?, Optional<T>> minBy(Comparator<? super T> comparator)
复制代码
还有更为通用的名为reducing的归约收集器,咱们就不介绍了,下面,看一些例子。
为了便于使用Collectors中的方法,咱们将其中的方法静态导入,即加入以下代码:
import static java.util.stream.Collectors.*;
复制代码
统计每一个年级的学生个数,代码能够为:
Map<String, Long> gradeCountMap = students.stream().collect(
groupingBy(Student::getGrade, counting()));
复制代码
统计一个单词流中每一个单词的个数,按出现顺序排序,代码示例为:
Map<String, Long> wordCountMap =
Stream.of("hello","world","abc","hello").collect(
groupingBy(Function.identity(), LinkedHashMap::new, counting()));
复制代码
获取每一个年级分数最高的一个学生,代码能够为:
Map<String, Optional<Student>> topStudentMap = students.stream().collect(
groupingBy(Student::getGrade,
maxBy(Comparator.comparing(Student::getScore))));
复制代码
须要说明的是,这个分组收集结果是Optional,而不是Student,这是由于maxBy处理的流多是空流,但对咱们的例子,这是不可能的,为了直接获得Student,可使用Collectors的另外一个收集器collectingAndThen,在获得Optional后调用Optional的get方法,以下所示:
Map<String, Student> topStudentMap = students.stream().collect(
groupingBy(Student::getGrade,
collectingAndThen(
maxBy(Comparator.comparing(Student::getScore)),
Optional::get)));
关于collectingAndThen,咱们待会再进一步讨论。
复制代码
除了基本的分组计数,还常常须要进行一些分组数值统计,好比求学生分数的和、平均分、最高分/最低分等,针对int,long和double类型,Collectors提供了专门的收集器,好比:
//求平均值,int和long也有相似方法
public static <T> Collector<T, ?, Double>
averagingDouble(ToDoubleFunction<? super T> mapper)
//求和,long和double也有相似方法
public static <T> Collector<T, ?, Integer>
summingInt(ToIntFunction<? super T> mapper)
//求多种汇总信息,int和double也有相似方法
//LongSummaryStatistics包括个数、最大值、最小值、和、平均值等多种信息
public static <T> Collector<T, ?, LongSummaryStatistics>
summarizingLong(ToLongFunction<? super T> mapper)
复制代码
好比,按年级统计学生分数信息,代码能够为:
Map<String, DoubleSummaryStatistics> gradeScoreStat =
students.stream().collect(
groupingBy(Student::getGrade,
summarizingDouble(Student::getScore)));
复制代码
对于每一个分组内的元素,咱们感兴趣的可能不是元素自己,而是它的某部分信息,在上节介绍的Stream API中,Stream有map方法,能够将元素进行转换,Collectors也为分组元素提供了函数mapping,以下所示:
public static <T, U, A, R>
Collector<T, ?, R> mapping(Function<? super T, ? extends U> mapper,
Collector<? super U, A, R> downstream)
复制代码
交给下游收集器downstream的再也不是元素自己,而是应用转换函数mapper以后的结果。好比,对学生按年级分组,获得学生名称列表,代码能够为:
Map<String, List<String>> gradeNameMap =
students.stream().collect(
groupingBy(Student::getGrade,
mapping(Student::getName, toList())));
System.out.println(gradeNameMap);
复制代码
输出为:
{1=[zhangsan, wangwu, sunqi], 2=[lisi, zhaoliu]}
复制代码
对分组后的元素,咱们能够计数,找最大/最小元素,计算一些数值特征,还能够转换后(map)再收集,那可不能够像上节介绍的Stream API同样,进行排序(sort)、过滤(filter)、限制返回元素(skip/limit)呢?Collector没有专门的收集器,但有一个通用的方法:
public static<T,A,R,RR> Collector<T,A,RR> collectingAndThen( Collector<T,A,R> downstream, Function<R,RR> finisher) 复制代码
这个方法接受一个下游收集器downstream和一个finisher,返回一个收集器,它的主要代码为:
return new CollectorImpl<>(downstream.supplier(),
downstream.accumulator(),
downstream.combiner(),
downstream.finisher().andThen(finisher),
characteristics);
复制代码
也就是说,它在下游收集器的结果上又调用了finisher。利用这个finisher,咱们能够实现多种功能,下面看一些例子。
收集完再排序,能够定义以下方法:
public static <T> Collector<T, ?, List<T>> collectingAndSort(
Collector<T, ?, List<T>> downstream,
Comparator<? super T> comparator) {
return Collectors.collectingAndThen(downstream, (r) -> {
r.sort(comparator);
return r;
});
}
复制代码
好比,将学生按年级分组,分组内学生按照分数由高到低进行排序,利用这个方法,代码能够为:
Map<String, List<Student>> gradeStudentMap =
students.stream().collect(
groupingBy(Student::getGrade,
collectingAndSort(toList(),
Comparator.comparing(Student::getScore).reversed())));
复制代码
针对这个需求,也能够先对流进行排序,而后再分组。
收集完再过滤,能够定义以下方法:
public static <T> Collector<T, ?, List<T>> collectingAndFilter(
Collector<T, ?, List<T>> downstream,
Predicate<T> predicate) {
return Collectors.collectingAndThen(downstream, (r) -> {
return r.stream().filter(predicate).collect(Collectors.toList());
});
}
复制代码
好比,将学生按年级分组,分组后,每一个分组只保留不及格的学生(低于60分),利用这个方法,代码能够为:
Map<String, List<Student>> gradeStudentMap =
students.stream().collect(
groupingBy(Student::getGrade,
collectingAndFilter(toList(), t->t.getScore()<60)));
复制代码
针对这个需求,也能够先对流进行过滤,而后再分组。
收集完,只返回特定区间的结果,能够定义以下方法:
public static <T> Collector<T, ?, List<T>> collectingAndSkipLimit(
Collector<T, ?, List<T>> downstream, long skip, long limit) {
return Collectors.collectingAndThen(downstream, (r) -> {
return r.stream().skip(skip).limit(limit).collect(Collectors.toList());
});
}
复制代码
好比,将学生按年级分组,分组后,每一个分组只保留前两名的学生,代码能够为:
Map<String, List<Student>> gradeStudentMap =
students.stream()
.sorted(Comparator.comparing(Student::getScore).reversed())
.collect(groupingBy(Student::getGrade,
collectingAndSkipLimit(toList(), 0, 2)));
复制代码
此次,咱们先对学生流进行了排序,而后再进行了分组。
分组的一个特殊状况是分区,就是将流按true/false分为两个组,Collectors有专门的分区函数:
public static <T> Collector<T, ?, Map<Boolean, List<T>>>
partitioningBy(Predicate<? super T> predicate)
public static <T, D, A> Collector<T, ?, Map<Boolean, D>>
partitioningBy(Predicate<? super T> predicate,
Collector<? super T, A, D> downstream)
复制代码
第一个的下游收集器为toList(),第二个能够指定一个下游收集器。
好比,将学生按照是否及格(大于等于60分)分为两组,代码能够为:
Map<Boolean, List<Student>> byPass = students.stream().collect(
partitioningBy(t->t.getScore()>=60));
复制代码
按是否及格分组后,计算每一个分组的平均分,代码能够为:
Map<Boolean, Double> avgScoreMap = students.stream().collect(
partitioningBy(t->t.getScore()>=60,
averagingDouble(Student::getScore)));
复制代码
groupingBy和partitioningBy均可以接受一个下游收集器,而下游收集器又能够是分组或分区。
好比,按年级对学生分组,分组后,再按照是否及格对学生进行分区,代码能够为:
Map<String, Map<Boolean, List<Student>>> multiGroup =
students.stream().collect(
groupingBy(Student::getGrade,
partitioningBy(t->t.getScore()>=60)));
复制代码
本节主要讨论了各类收集器,包括容器收集器、字符串收集器、分组和分区收集器等。
对于分组和分区,它们接受一个下游收集器,对同一个分组或分区内的元素进行进一步收集,下游收集器还能够是分组或分区,以构建多级分组,有一些收集器主要用于分组,好比counting, maxBy, minBy, summarizingDouble等。
mapping和collectingAndThen也都接受一个下游收集器,mapping在把元素交给下游收集器以前先进行转换,而collectingAndThen对下游收集器的结果进行转换,组合利用它们,能够构造更为灵活强大的收集器。
至此,关于Java 8中的函数式数据处理Stream API,咱们就介绍完了,Stream API提供了集合数据处理的经常使用函数,利用它们,能够简洁地实现大部分常见需求,大大减小代码,提升可读性。
对于并发编程,Java 8也提供了一个新的类CompletableFuture,相似于Stream API对集合数据的流水线式操做,使用CompletableFuture,能够实现对多个异步任务进行流水线式操做,它具体是什么呢?
(与其余章节同样,本节全部代码位于 github.com/swiftma/pro…,位于包shuo.laoma.java8.c93下)
未完待续,查看最新文章,敬请关注微信公众号“老马说编程”(扫描下方二维码),从入门到高级,深刻浅出,老马和你一块儿探索Java编程及计算机技术的本质。用心原创,保留全部版权。