【Java8新特性】——Stream的reduce及Collect使用方式


前言

本文主要讲解关于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做汇总后返回。

        System.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():
而Characteristics是Collector内的一个枚举类,声明了CONCURRENT、UNORDERED、IDENTITY_FINISH等三个属性,用来约束Collector的属性。

CONCURRENT:表示此收集器支持并发,意味着允许在多个线程中,累加器可以调用结果容器
UNORDERED:表示收集器并不按照Stream中的元素输入顺序执行
IDENTITY_FINISH:表示finisher实现的是识别功能,可忽略。
注:如果一个容器仅声明CONCURRENT属性,而不是UNORDERED属性,那么该容器仅仅支持无序的Stream在多线程中执行。

四、定制收集器

定制收集器需要实现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);
}

在这里插入图片描述
第二步结合之前操作的结果和当前值,生成并返回新的值,即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自定义起来,也不是特别的麻烦,不过要明确以下几点:

  1. 参数类型:这里最重要的是指定累加器的类型,一般都是自定义的过度类
    待收集元素的类型:T;
    累加器的类型:A;
    最终结果的类型:R。
  2. 累加器的逻辑
  3. 最终结果的转换
  4. Collector特征的选择

具体demo推荐博文java8之定制收集器

总结

主要讲reduce三种使用方式,其中方式1,2两种变形的区别在于返回值是否可以为空,变形二因为在初始化已经赋有默认值,则不存在为空的可能,方式3与前者的区别要注意在并行情况下的使用,在提升效率的同时,注意是否预期效果相符。关于collect本质上就是收集器,通过收集元素,累加器的处理以及最后结果转换,达到复用的效果,同时提升使用效率。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Java 8引入了Stream API,它是一种新的处理集合的方式,可以用更简洁、更易读的代码处理集合数据。Stream API提供了非常方便的、高效的数据处理方式,包括筛选、排序、映射、归约等。 下面是一些Stream的常用操作: 1. filter():筛选符合条件的元素 ```java List<String> list = Arrays.asList("apple", "orange", "banana", "pear", "peach"); List<String> result = list.stream().filter(str -> str.contains("e")).collect(Collectors.toList()); ``` 2. map():将元素转换成其他形式或提取信息 ```java List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5); List<Integer> result = nums.stream().map(num -> num * num).collect(Collectors.toList()); ``` 3. sorted():对元素进行排序 ```java List<Integer> nums = Arrays.asList(5, 3, 1, 2, 4); List<Integer> result = nums.stream().sorted().collect(Collectors.toList()); ``` 4. distinct():去重 ```java List<Integer> nums = Arrays.asList(1, 2, 3, 3, 4, 4, 5); List<Integer> result = nums.stream().distinct().collect(Collectors.toList()); ``` 5. limit():截取流中前n个元素 ```java List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5); List<Integer> result = nums.stream().limit(3).collect(Collectors.toList()); ``` 6. skip():跳过流中前n个元素 ```java List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5); List<Integer> result = nums.stream().skip(3).collect(Collectors.toList()); ``` 7. reduce():将流中元素归约为一个值 ```java List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5); int result = nums.stream().reduce(0, (a, b) -> a + b); ``` 这些操作只是Stream API中的一部分,还有很多其他操作可以使用Stream API可以让我们更加方便地处理集合数据,提高开发效率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mandy_i

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值