文章目录
- 前言
- 一、Reduce
- 1.1一个参数的Reduce
- BiFunction
- BinaryOperator
- 1.2二个参数的Reduce
- 1.3三个参数的Reduce
- 非并行
- 并行
- 二、Collect
- BiConsumer
- 三、Collector
- 四、定制收集器
- 总结
前言
本文主要讲解关于Stream中reduce的使用方式以及Collect使用方式,同时展示如何自定义收集器。
提示:如果大家对lambda表达式中的四大基础函数不清楚,推荐大家优先看下四大内置核心函数式接口以及看下关于reduce相关api的使用,Java8 中reduce的基本使用
一、Reduce
Reduce中文含义为:减少、缩小;而Stream中的Reduce方法干的正是这样的活:根据一定的规则将Stream中的元素进行计算后返回一个唯一的值。
它有三个变种,输入参数分别是一个参数、二个参数以及三个参数;
1.1一个参数的Reduce
Optional<T> reduce(BinaryOperator<T> accumulator)
先解释基本概念
BiFunction
R apply(T t, U u);
函数式接口与Function不同点在于它接收两个输入返回一个输出;Function接收一个输入返回一个输出。两个输入,一个输出的类型可以不同。
BinaryOperator
public interface BinaryOperator<T> extends BiFunction<T,T,T>
继承BiFunction,相比BinaryOperator直接限定其三个参数必须一样,表示两个相同类型的输入经过计算后产生一个同类型的输出。
BinaryOperator接口,可以看到reduce方法接受一个函数,这个函数有两个参数,第一个参数是上次函数执行的返回值(也称为中间结果),第二个参数是stream中的元素,函数将两个值按照方法处理,得到值赋给下次执行这个函数的参数。第一次执行的时候第一参数的值是stream的第一元素,第二个元素是stream的第二元素,因为stream元素集合可能为空,所以这个方法的返回值为Optional。举个例子感受一下
Optional accResult = Stream.of(1, 2, 3)
.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
--------
accResult: 6
--------
1.2二个参数的Reduce
T reduce(T identity, BinaryOperator<T> accumulator);
与第一个变形不同的,会接受一个返回值类型相同identity参数,用于指定Stream的循环初始值,如果Stream为空,则默认返回identity,则不会再返回null值。
int accResult = Stream.of(1, 2, 3)
.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
--------
accResult: 6
--------
1.3三个参数的Reduce
<U> U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> combiner);
解释过前两个值的概念,第三个值combiner作用,主要用于并发进行的 为了避免竞争 每个reduce线程都会有独立的result combiner的作用在于合并每个线程的result得到最终结果。如果Stream是非并行时,第三个参数实际上是不生效的。
但是如果Stream是并行,第三个参数有了意义,将不同线程计算的结果调用combiner做汇总后返回。
.out.println("----------stream---------");
System.out.println(Stream.of(1, 2, 3).reduce(4, (s1, s2) -> s1 + s2
, (s1, s2) -> s1 + s2));
System.out.println("----------parallel stream---------");
System.out.println(Stream.of(1, 2, 3).parallel().reduce(4, (s1, s2) -> s1 + s2
, (s1, s2) -> s1 + s2));
----------stream---------
10
----------parallel stream---------
18
此时看下非并行和并行的打印结果,运算结果并不一样,原因在哪?
非并行
计算过程:第一步计算4 + 1 = 5,第二步是5 + 2 = 7,第三步是7 + 3 = 10。按非并行的方式来看它是分了三步的,每一步都要依赖前一步的运算结果。
并行
计算过程:并行计算时,线程之间没有影响,因此每个线程在调用第二个参数BiFunction进行计算时,直接都是使用result值当其第一个参数(由于Stream计算的惰性处理,在调用最终方法前,都不会进行实际的运算,因此每个线程取到的result值都是原始的4),因此计算过程现在是这样的:线程1:1 + 4 = 5;线程2:2 + 4 = 6;线程3:3 + 4 = 7;Combiner函数: 5 + 6 + 7 = 18
二、Collect
collect就是收集器,是Stream一种通用的、从流生成复杂值的结构。只要将它传给collect方法,也就是所谓的转换方法,其就会生成想要的数据结构。
<R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner);
- supplier:动态的提供初始化的值;创建一个可变的结果容器(JAVADOC);对于并行计算,这个方法可能被调用多次,每次返回一个新的对象;
- accumulator:类型为BiConsumer,注意这个接口是没有返回值的;它必须将一个元素放入结果容器中(JAVADOC)。
- combiner:类型也是BiConsumer,因此也没有返回值。它与三参数的Reduce类型,只是在并行计算时汇总不同线程计算的结果。它的输入是两个结果容器,必须将第二个结果容器中的值全部放入第一个结果容器中(JAVADOC)。
BiConsumer
public interface BiConsumer<T, U> {
void accept(T t, U u);
}
可见它就是一个两个输入参数的Consumer的变种。计算没有返回值。即消费型。
与reduce一样,同样分为并行和非并行,下面讲解下并行
/**
* 模拟Filter查找其中含有字母a的所有元素,打印结果将是aa ab ad
*/
Stream<String> s1 = Stream.of("aa", "ab", "c", "ad");
Predicate<String> predicate = t -> t.contains("a");
System.out.println(s1.parallel().collect(() -> new ArrayList<String>(),
(array, s) -> {if (predicate.test(s)) array.add(s); },
(array1, array2) -> array1.addAll(array2)));
每个线程都创建了一个结果容器ArrayList,假设每个线程处理一个元素,那么处理的结果将会是[aa],[ab],[],[ad]四个结果容器(ArrayList);最终再调用第三个BiConsumer参数将结果全部Put到第一个List中,因此返回结果就是打印的结果了。
三、Collector
Collector是Stream的可变减少操作接口,可变减少操作包括:将元素累积到集合中,使用StringBuilder连接字符串;计算元素相关的统计信息,例如sum,min,max或average等。Collectors这个工具库,在该库中封装了相应的转换方法。当然,Collectors工具库仅仅封装了常用的一些情景,如果有特殊需求,那就要自定义了。
Collector<T, A, R>接受三个泛型参数,对可变减少操作的数据类型作相应限制:
T:输入元素类型
A:缩减操作的可变累积类型(通常隐藏为实现细节)
R:可变减少操作的结果类型
Collector接口声明了4个函数,这四个函数一起协调执行以将元素目累积到可变结果容器中,并且可以选择地对结果进行最终的变换.
Supplier<A> supplier(): 创建新的结果结
BiConsumer<A, T> accumulator(): 将元素添加到结果容器
BinaryOperator<A> combiner(): 将两个结果容器合并为一个结果容器
Function<A, R> finisher(): 对结果容器作相应的变换
Collector接口声明了4个函数,这四个函数一起协调执行以将元素目累积到可变结果容器中,并且可以选择地对结果进行最终的变换.
在Collector接口的characteristics方法内,可以对Collector声明相关约束
Set<Characteristics> characteristics():
四、定制收集器
定制收集器需要实现Collector接口
实现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;
}
}
对应着Collector中四个流程来完成
Supplier<A> supplier(): 创建新的结果结
BiConsumer<A, T> accumulator(): 将元素添加到结果容器
BinaryOperator<A> combiner(): 将两个结果容器合并为一个结果容器
Function<A, R> finisher():
第一步生成新的结果集Supplier supplier(): 创建新的结果结
public Supplier<StringJoiner> supplier() {
return () -> new StringJoiner(delim, prefix, suffix);
}
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiQDOxEzX3xCZlhXam9VbsUmepNXZy9CXwJWZ3xCdh1mcvZ2Lc1zaHRGcWdUYuVzVa9GczoVdG1mWfVGc5RHLwIzX39GZhh2csATMflHLwEzX4xSZz91ZsAzMfRHLGZkRGZkRfJ3bs92YskmNhVTYykVNQJVMRhXVEF1X0hXZ0xiNx8VZ6l2cssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnLyYTO2IzMldDM4gTM1MTZyYzXyMDOxETM5IzLcdDMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.png)
第二步结合之前操作的结果和当前值,生成并返回新的值,即BiConsumer<A, T> accumulator()
public BiConsumer<StringJoiner, String> accumulator() {
return StringJoiner::add;
}
第三步将两个容器合并,由于Collector支持并发操作,如果不将多的容器合并,必然会导致数据的混乱。如果仅仅在串行执行,此步骤可以省略。即BinaryOperator combiner()
public BinaryOperator<StringJoiner> combiner() {
return StringJoiner::merge;
}
第四步处理返回值,即Function<A, R> finisher(): 对结果容器作相应的变换。
public Function<StringJoiner, String> finisher() {
return StringJoiner::toString;
}
Collector自定义起来,也不是特别的麻烦,不过要明确以下几点:
-
参数类型:这里最重要的是指定累加器的类型,一般都是自定义的过度类
待收集元素的类型:T;
累加器的类型:A;
最终结果的类型:R。
- 累加器的逻辑
- 最终结果的转换
- Collector特征的选择