Reduce介绍
reduce 操作可以实现从Stream中生成一个值,其生成的值不是随意的,而是根据指定的计算模型。比如,之前提到count、min和max方法,因为常用而被纳入标准库中。事实上,这些方法都是reduce操作。
reduce方法有三个override的方法:
Optional<T> reduce(BinaryOperator<T> accumulator);
T reduce(T identity, BinaryOperator<T> accumulator);
<U> U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> combiner);
我们先看第一个变形,其接受一个函数接口BinaryOperator<T>,而这个接口又继承于BiFunction<T, T, T>.在BinaryOperator接口中,又定义了两个静态方法minBy和maxBy。这里我们先不管这两个静态方法,先了解reduce的操作。
@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T> {
public static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator) {
Objects.requireNonNull(comparator);
return (a, b) -> comparator.compare(a, b) <= 0 ? a : b;
}
public static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator) {
Objects.requireNonNull(comparator);
return (a, b) -> comparator.compare(a, b) >= 0 ? a : b;
}
}
在使用时,我们可以使用Lambada表达式来表示
BinaryOperator接口,可以看到reduce方法接受一个函数,这个函数有两个参数,第一个参数是上次函数执行的返回值(也称为中间结果),第二个参数是stream中的元素,这个函数把这两个值相加,得到的和会被赋值给下次执行这个函数的第一个参数。要注意的是:第一次执行的时候第一个参数的值是Stream的第一个元素,第二个参数是Stream的第二个元素。这个方法返回值类型是Optional,
Optional accResult = Stream.of(1, 2, 3, 4)
.reduce((acc, item) -> {
System.out.println("acc : " + acc);
acc += item;
System.out.println("item: " + item);
System.out.println("acc+ : " + acc);
System.out.println("--------");
return acc;
});
System.out.println("accResult: " + accResult.get());
System.out.println("--------");
// 结果打印
acc : 1
item: 2
acc+ : 3
--------
acc : 3
item: 3
acc+ : 6
--------
acc : 6
item: 4
acc+ : 10
--------
accResult: 10
--------
下面来看第二个变形,与第一种变形相同的是都会接受一个BinaryOperator函数接口,不同的是其会接受一个identity参数,用来指定Stream循环的初始值。如果Stream为空,就直接返回该值。另一方面,该方法不会返回Optional,因为该方法不会出现null。
int accResult = Stream.of(1, 2, 3, 4)
.reduce(0, (acc, item) -> {
System.out.println("acc : " + acc);
acc += item;
System.out.println("item: " + item);
System.out.println("acc+ : " + acc);
System.out.println("--------");
return acc;
});
System.out.println("accResult: " + accResult);
System.out.println("--------");
// 结果打印
acc : 0
item: 1
acc+ : 1
--------
acc : 1
item: 2
acc+ : 3
--------
acc : 3
item: 3
acc+ : 6
--------
acc : 6
item: 4
acc+ : 10
--------
accResult: 10
--------
从打印结果可以看出,reduce前两种变形,因为接受参数不同,其执行的操作也有相应变化:
变形1,未定义初始值,从而第一次执行的时候第一个参数的值是Stream的第一个元素,第二个参数是Stream的第二个元素
变形2,定义了初始值,从而第一次执行时候第一个参数的值是初始值,第二个参数是Stream的第一个元素
对于第三种变形,我们先看各个参数的含义,第一个参数返回实例u,传递你要返回的U类型对象的初始化实例u,第二个参数累加器accumulator,可以使用二元表达式(即二元lambda表达式),声明你在u上累加你的数据来源t的逻辑,例如(u,t)->u.sum(t),此时lambda表达式的行参列表是返回实例u和遍历的集合元素t,函数体是在u上累加t,第三个参数组合器combiner,同样是二元表达式,(u,t)->u。
ArrayList<Integer> accResult_ = Stream.of(1, 2, 3, 4)
.reduce(new ArrayList<Integer>(),
new BiFunction<ArrayList<Integer>, Integer, ArrayList<Integer>>() {
@Override
public ArrayList<Integer> apply(ArrayList<Integer> acc, Integer item) {
acc.add(item);
System.out.println("item: " + item);
System.out.println("acc+ : " + acc);
System.out.println("BiFunction");
return acc;
}
}, new BinaryOperator<ArrayList<Integer>>() {
@Override
public ArrayList<Integer> apply(ArrayList<Integer> acc, ArrayList<Integer> item) {
System.out.println("BinaryOperator");
acc.addAll(item);
System.out.println("item: " + item);
System.out.println("acc+ : " + acc);
System.out.println("--------");
return acc;
}
});
System.out.println("accResult_: " + accResult_);
// 结果打印
item: 1
acc+ : [1]
BiFunction
item: 2
acc+ : [1, 2]
BiFunction
item: 3
acc+ : [1, 2, 3]
BiFunction
item: 4
acc+ : [1, 2, 3, 4]
BiFunction
accResult_: [1, 2, 3, 4]
accResult_: 10
首先示例代码中,传递给第一个参数是ArrayList,在第二个函数参数中打印了“BiFunction”,而在第三个参数接口中打印了函数接口中打印了”BinaryOperator“.可是,看打印结果,只是打印了“BiFunction”,而没有打印”BinaryOperator“,说明第三个函数参数病没有执行。这里我们知道了该变形可以返回任意类型的数据。对于第三个函数参数,为什么没有执行,刚开始的时候也是没有看懂到底是啥意思呢,而且其参数必须为返回的数据类型?看了好几遍文档也是一头雾水。在 java8 reduce方法中的第三个参数combiner有什么作用?这里找到了答案,Stream是支持并发操作的,为了避免竞争,对于reduce线程都会有独立的result,combiner的作用在于合并每个线程的result得到最终结果。这也说明了了第三个函数参数的数据类型必须为返回数据类型了。
需要注意的是,因为第三个参数用来处理并发操作,如何处理数据的重复性,应多做考虑,否则会出现重复数据!
Collect介绍
概述
前面我们使用过collect(toList()),在流中生成列表。实际开发过程中,List又是我们经常用到的数据结构,但是有时候我们也希望Stream能够转换生成其他的值,比如Map或者set,甚至希望定制生成想要的数据结构。
collect也就是收集器,是Stream一种通用的、从流生成复杂值的结构。只要将它传给collect方法,也就是所谓的转换方法,其就会生成想要的数据结构。这里不得不提下,Collectors这个工具库,在该库中封装了相应的转换方法。当然,Collectors工具库仅仅封装了常用的一些情景,如果有特殊需求,那就要自定义了。
显然,List是能想到的从流中生成的最自然的数据结构, 但是有时人们还希望从流生成其他值, 比如 Map 或 Set, 或者你希望定制一个类将你想要的东西抽象出来。
前面已经讲过,仅凭流上方法的签名,就能判断出这是否是一个及早求值的操作。 reduce操作就是一个很好的例子, 但有时人们希望能做得更多。
这就是收集器,一种通用的、从流生成复杂值的结构。只要将它传给collect 方法,所有的流就都可以使用它了。
<R, A> R collect(Collector<? super T, A, R> collector);
<R> R collect(Supplier<R> supplier,BiConsumer<R, ? super T> accumulator,BiConsumer<R, R> combiner);
转成值
使用collect可以将Stream转换成值。maxBy和minBy允许用户按照某个特定的顺序生成一个值。
averagingDouble:求平均值,Stream的元素类型为double
averagingInt:求平均值,Stream的元素类型为int
averagingLong:求平均值,Stream的元素类型为long
counting:Stream的元素个数
maxBy:在指定条件下的,Stream的最大元素
minBy:在指定条件下的,Stream的最小元素
reducing: reduce操作
summarizingDouble:统计Stream的数据(double)状态,其中包括count,min,max,sum和平均。
summarizingInt:统计Stream的数据(int)状态,其中包括count,min,max,sum和平均。
summarizingLong:统计Stream的数据(long)状态,其中包括count,min,max,sum和平均。
summingDouble:求和,Stream的元素类型为double
summingInt:求和,Stream的元素类型为int
summingLong:求和,Stream的元素类型为long
示例:
Optional<Integer> collectMaxBy = Stream.of(1, 2, 3, 4)
.collect(Collectors.maxBy(Comparator.comparingInt(e -> e)));
System.out.println("collectMaxBy:" + collectMaxBy.get());
// 打印结果
// collectMaxBy:4
分割数据块
collect的一个常用操作将Stream分解成两个集合。假如一个数字的Stream,我们可能希望将其分割成两个集合,一个是偶数集合,另外一个是奇数集合。我们首先想到的就是过滤操作,通过两次过滤操作,很简单的就完成了我们的需求。
但是这样操作起来有问题。首先,为了执行两次过滤操作,需要有两个流。其次,如果过滤操作复杂,每个流上都要执行这样的操作, 代码也会变得冗余。
这里我们就不得不说Collectors库中的partitioningBy方法,它接受一个流,并将其分成两部分:使用Predicate对象,指定条件并判断一个元素应该属于哪个部分,并根据布尔值返回一个Map到列表。因此对于key为true所对应的List中的元素,满足Predicate对象中指定的条件;同样,key为false所对应的List中的元素,不满足Predicate对象中指定的条件
这样,使用partitioningBy,我们就可以将数字的Stream分解成奇数集合和偶数集合了。
Map<Boolean, List<Integer>> collectParti = Stream.of(1, 2, 3, 4)
.collect(Collectors.partitioningBy(it -> it % 2 == 0));
System.out.println("collectParti : " + collectParti);
// 打印结果
// collectParti : {false=[1, 3], true=[2, 4]}
数据分组
数据分组是一种更自然的分割数据操作, 与将数据分成true和false两部分不同,可以使用任意值对数据分组。
调用Stream的collect方法,传入一个收集器,groupingBy接受一个分类函数,用来对数据分组,就像partitioningBy一样,接受一个Predicate对象将数据分成true和false两部分。我们使用的分类器是一个Function对象,和map操作用到的一样。
示例:
Map<Boolean, List<Integer>> collectGroup= Stream.of(1, 2, 3, 4)
.collect(Collectors.groupingBy(it -> it > 3));
System.out.println("collectGroup : " + collectGroup);
// 打印结果
// collectGroup : {false=[1, 2, 3], true=[4]}
注:
看groupingBy和partitioningBy的例子,他们的效果都是一样的,都是将Stream的数据进行了分割处理并返回一个Map。可能举的例子给你带来了误区,实际上他们两个完全是不一样的。
partitioningBy是根据指定条件,将Stream分割,返回的Map为Map
字符串
有时候,我们将Stream的元素(String类型)最后生成一组字符串。比如在Stream.of(“1”, “2”, “3”, “4”)中,将Stream格式化成“1,2,3,4”。
如果不使用Stream,我们可以通过for循环迭代实现。
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
StringBuilder sb = new StringBuilder();
for (Integer it : list) {
if (sb.length() > 0) {
sb.append(",");
}
sb.append(it);
}
System.out.println(sb.toString());
// 打印结果
// 1,2,3,4
在Java 1.8中,我们可以使用Stream来实现。这里我们将使用 Collectors.joining 收集Stream中的值,该方法可以方便地将Stream得到一个字符串。joining函数接受三个参数,分别表示允(用以分隔元素)、前缀和后缀。
示例:
String strJoin = Stream.of("1", "2", "3", "4")
.collect(Collectors.joining(",", "[", "]"));
System.out.println("strJoin: " + strJoin);
// 打印结果
// strJoin: [1,2,3,4]
组合Collector
前面,我们已经了解到Collector的强大,而且非常的使用。如果将他们组合起来,是不是更厉害呢?看前面举过的例子,在数据分组时,我们是得到的分组后的数据列表 collectGroup : {false=[1, 2, 3], true=[4]}。如果我们的要求更高点,我们不需要分组后的列表,只要得到分组后列表的个数就好了。
这时候,很多人下意识的都会想到,遍历Map就好了,然后使用list.size(),就可以轻松的得到各个分组的列表个数。
// 分割数据块
Map<Boolean, List<Integer>> collectParti = Stream.of(1, 2, 3, 4)
.collect(Collectors.partitioningBy(it -> it % 2 == 0));
Map<Boolean, Integer> mapSize = new HashMap<>();
collectParti.entrySet()
.forEach(entry -> mapSize.put(entry.getKey(), entry.getValue().size()));
System.out.println("mapSize : " + mapSize);
// 打印结果
// mapSize : {false=2, true=2}
在partitioningBy方法中,有这么一个变形:
Map<Boolean, Long> partiCount = Stream.of(1, 2, 3, 4)
.collect(Collectors.partitioningBy(it -> it.intValue() % 2 == 0,
Collectors.counting()));
System.out.println("partiCount: " + partiCount);
// 打印结果
// partiCount: {false=2, true=2}
在partitioningBy方法中,我们不仅传递了条件函数,同时传入了第二个收集器,用以收集最终结果的一个子集,这些收集器叫作下游收集器。收集器是生成最终结果的一剂配方,下游收集器则是生成部分结果的配方,主收集器中会用到下游收集器。这种组合使用收集器的方式, 使得它们在 Stream 类库中的作用更加强大。
那些为基本类型特殊定制的函数,如averagingInt、summarizingLong等,事实上和调用特殊Stream上的方法是等价的,加上它们是为了将它们当作下游收集器来使用的。