关于java:归约分组与分区深入讲解JavaStream终结操作

3次阅读

共计 7814 个字符,预计需要花费 20 分钟才能阅读完成。

思维导图镇楼。

上一篇中给大家讲了 Stream 的前半部分常识——包含对 Stream 的整体概览及 Stream 的创立和 Stream 的转换流操作,并对 Stream 一些外部优化点做了扼要的阐明。

虽迟但到,明天就来持续给大家更 Stream 第二局部常识——终结操作,因为这部分的 API 内容繁多且简单,所以我单开一篇给大家细细讲讲。

正式开始之前,咱们先来说说聚合办法自身的个性(接下来我将用聚合办法代指终结操作中的办法):

  1. 聚合办法代表着整个流计算的最终后果,所以它的返回值都不是 Stream。
  2. 聚合办法返回值可能为空,比方 filter 没有匹配到的状况,JDK8 中用 Optional 来躲避 NPE。
  3. 聚合办法都会调用 evaluate 办法,这是一个外部办法,看源码的过程中能够用它来断定一个办法是不是聚合办法。

ok,通晓了聚合办法的个性,我为了便于了解,又将聚合办法分为几大类:

其中简略聚合办法我会简略解说,其它则会着重解说,尤其是收集器,它能做的切实太多了。。。

Stream 的聚合办法是咱们在应用 Stream 中的必用操作,认真学习本篇,不说马上就能对 Stream 得心应手,起码也能够行云流水吧😄

1. 简略聚合办法

第一节嘛,先来点简略的。

Stream 的聚合办法比上一篇讲过的无状态和有状态办法都要多,然而其中也有一些是喵一眼就能学会的,第一节咱们先来说说这部分办法:

  • count():返回 Stream 中元素的 size 大小。
  • forEach():通过外部循环 Stream 中的所有元素,对每一个元素进行生产,此办法没有返回值。
  • forEachOrder():和下面办法的成果一样,然而这个能够放弃生产程序,哪怕是在多线程环境下。
  • anyMatch(Predicate predicate):这是一个短路操作,通过传入断言参数判断是否有元素可能匹配上断言。
  • allMatch(Predicate predicate):这是一个短路操作,通过传入断言参数返回是否所有元素都能匹配上断言。
  • noneMatch(Predicate predicate):这是一个短路操作,通过传入断言参数判断是否所有元素都无奈匹配上断言,如果是则返回 true,反之则 false。
  • findFirst():这是一个短路操作,返回 Stream 中的第一个元素,Stream 可能为空所以返回值用 Optional 解决。
  • findAny():这是一个短路操作,返回 Stream 中的任意一个元素,串型流中个别是第一个元素,Stream 可能为空所以返回值用 Optional 解决。

尽管以上都比较简单,然而这外面有五个波及到短路操作的办法我还是想提两嘴:

首先是 findFirst()findAny()这两个办法,因为它们只须要拿到一个元素就能办法就能完结,所以短路成果很好了解。

接着是 anyMatch 办法,它只须要匹配到一个元素办法也能完结,所以它的短路成果也很好了解。

最初是 allMatch 办法和noneMatch,乍一看这两个办法都是须要遍历整个流中的所有元素的,其实不然,比方 allMatch 只有有一个元素不匹配断言它就能够返回 false 了,noneMatch 只有有一个元素匹配上断言它也能够返回 false 了,所以它们都是具备短路成果的办法。

2. 归约

2.1 reduce:重复求值

第二节咱们来说说归约,因为这个词过于形象,我不得不找了一句通俗易懂的解释来翻译这句话,上面是归约的定义:

将一个 Stream 中的所有元素重复联合起来,失去一个后果,这样的操作被称为归约。

注:在函数式编程中,这叫做折叠(fold)。

举个很简略的例子,我有 1、2、3 三个元素,我把它们俩俩相加,最初得出 6 这个数字,这个过程就是归约。

再比方,我有 1、2、3 三个元素,我把它们俩俩比拟,最初挑出最大的数字 3 或者挑出最小的数字 1,这个过程也是归约。

上面我举一个求和的例子来演示归约,归约应用 reduce 办法:

        Optional<Integer> reduce = List.of(1, 2, 3).stream()
                .reduce((i1, i2) -> i1 + i2);

首先你可能留神到了,我在上文的小例子中始终在用俩俩这个词,这代表归约是俩俩的元素进行解决而后失去一个最终值,所以 reduce 的办法的参数是一个二元表达式,它将两个参数进行任意解决,最初失去一个后果,其中它的参数和后果必须是同一类型。

比方代码中的,i1 和 i2 就是二元表达式的两个参数,它们别离代表元素中的第一个元素和第二个元素,当第一次相加实现后,所得的后果会赋值到 i1 身上,i2 则会持续代表下一个元素,直至元素耗尽,失去最终后果。

如果你感觉这么写不够优雅,也能够应用 Integer 中的默认办法:

        Optional<Integer> reduce = List.of(1, 2, 3).stream()
                .reduce(Integer::sum);

这也是一个以 办法援用 代表 lambda 表达式的例子。

你可能还留神到了,它们的返回值是 Optional 的,这是预防 Stream 没有元素的状况。

你也能够想方法去掉这种状况,那就是让元素中至多要有一个值,这里 reduce 提供一个重载办法给咱们:

        Integer reduce = List.of(1, 2, 3).stream()
                .reduce(0, (i1, i2) -> i1 + i2);

如上例,在二元表达式后面多加了一个参数,这个参数被称为初始值,这样哪怕你的 Stream 没有元素它最终也会返回一个 0,这样就不须要 Optional 了。

在理论办法运行中,初始值会在第一次执行中占据 i1 的地位,i2 则代表 Stream 中的第一个元素,而后所得的和再次占据 i1 的地位,i2 代表下一个元素。

不过应用初始值不是没有老本的,它应该合乎一个准则:accumulator.apply(identity, i1) == i1,也就是说在第一次执行的时候,它的返回后果都应该是你 Stream 中的第一个元素。

比方我下面的例子是一个相加操作,则第一次相加时就是0 + 1 = 1,合乎下面的准则,作此准则是为了保障并行流状况下可能失去正确的后果。

如果你的初始值是 1,则在并发状况下每个线程的初始化都是 1,那么你的最终和就会比你料想的后果要大。

2.2 max:利用归约求最大

max 办法也是一个归约办法,它是间接调用了 reduce 办法。

先来看一个示例:

        Optional<Integer> max = List.of(1, 2, 3).stream()
                .max((a, b) -> {if (a > b) {return 1;} else {return -1;}
                });

没错,这就是 max 办法用法,这让我感觉我不是在应用函数式接口,当然你也能够应用 Integer 的办法进行简化:

        Optional<Integer> max = List.of(1, 2, 3).stream()
                .max(Integer::compare);

哪怕如此,这个办法仍旧让我感觉到很繁琐,我尽管能够了解在 max 办法外面传参数是为了让咱们本人自定义排序规定,但我不了解为什么没有一个默认依照天然排序进行排序的办法,而是非要让我传参数。

直到起初我想到了根底类型 Stream,果然,它们外面是能够无需传参间接拿到最大值:

        OptionalLong max = LongStream.of(1, 2, 3).max();

果然,我能想到的,类库设计者都想到了~

:OptionalLong 是 Optional 对根底类型 long 的封装。

2.3 min:利用归约求最小

min 还是间接看例子吧:

        Optional<Integer> max = List.of(1, 2, 3).stream()
                .min(Integer::compare);

它和 max 区别就是底层把 > 换成了 <,过于简略,不再赘述。

3. 收集器

第三节咱们来看看收集器,它的作用是对 Stream 中的元素进行收集而造成一个新的汇合。

尽管我在本篇结尾的时候曾经给过一张思维导图了,然而因为收集器的 API 比拟多所以我又画了一张,算是对结尾那张的补充:

收集器的办法名是 collect,它的办法定义如下:

    <R, A> R collect(Collector<? super T, A, R> collector);

顾名思义,收集器是用来收集 Stream 的元素的,最初收集成什么咱们能够自定义,然而咱们个别不须要本人写,因为 JDK 内置了一个 Collector 的实现类——Collectors。

3.1 收集办法

通过 Collectors 咱们能够利用它的内置办法很不便的进行数据收集:

比方你想把元素收集成汇合,那么你能够应用 toCollection 或者 toList 办法,不过咱们个别不应用 toCollection,因为它须要传参数,没人喜爱传参数。

你也能够应用 toUnmodifiableList,它和 toList 区别就是它返回的汇合不能够扭转元素,比方删除或者新增。

再比方你要把元素去重之后收集起来,那么你能够应用 toSet 或者 toUnmodifiableSet。

接下来放一个比较简单的例子:

        // toList
        List.of(1, 2, 3).stream().collect(Collectors.toList());

        // toUnmodifiableList
        List.of(1, 2, 3).stream().collect(Collectors.toUnmodifiableList());

        // toSet
        List.of(1, 2, 3).stream().collect(Collectors.toSet());

        // toUnmodifiableSet
        List.of(1, 2, 3).stream().collect(Collectors.toUnmodifiableSet());

以上这些办法都没有参数,拿来即用,toList 底层也是经典的 ArrayList,toSet 底层则是经典的 HashSet。


兴许有时候你兴许想要一个收集成一个 Map,比方通过将订单数据转成一个订单号对应一个订单,那么你能够应用 toMap():

        List<Order> orders = List.of(new Order(), new Order());

        Map<String, Order> map = orders.stream()
                .collect(Collectors.toMap(Order::getOrderNo, order -> order));

toMap() 具备两个参数:

  1. 第一个参数代表 key,它示意你要设置一个 Map 的 key,我这里指定的是元素中的 orderNo。
  2. 第二个参数代表 value,它示意你要设置一个 Map 的 value,我这里间接把元素自身当作值,所以后果是一个 Map<String, Order>。

你也能够将元素的属性当作值:

        List<Order> orders = List.of(new Order(), new Order());

        Map<String, List<Item>> map = orders.stream()
                .collect(Collectors.toMap(Order::getOrderNo, Order::getItemList));

这样返回的就是一个订单号 + 商品列表的 Map 了。

toMap() 还有两个伴生办法:

  • toUnmodifiableMap():返回一个不可批改的 Map。
  • toConcurrentMap():返回一个线程平安的 Map。

这两个办法和 toMap() 的参数截然不同,惟一不同的就是底层生成的 Map 个性不太一样,咱们个别应用简简单单的 toMap() 就够了,它的底层是咱们最罕用的 HashMap() 实现。

toMap() 性能尽管弱小也很罕用,然而它却有一个致命毛病。

咱们晓得 HahsMap 遇到雷同的 key 会进行笼罩操作,然而 toMap() 办法生成 Map 时如果你指定的 key 呈现了反复,那么它会间接抛出异样。

比方下面的订单例子中,咱们假如两个订单的订单号一样,然而你又将订单号指定了为 key,那么该办法会间接抛出一个 IllegalStateException,因为它不容许元素中的 key 是雷同的。

3.2 分组办法

如果你想对数据进行分类,然而你指定的 key 是能够反复的,那么你应该应用 groupingBy 而不是 toMap。

举个简略的例子,我想对一个订单汇合以订单类型进行分组,那么能够这样:

        List<Order> orders = List.of(new Order(), new Order());

        Map<Integer, List<Order>> collect = orders.stream()
                .collect(Collectors.groupingBy(Order::getOrderType));

间接指定用于分组的元素属性,它就会主动依照此属性进行分组,并将分组的后果收集为一个 List。

        List<Order> orders = List.of(new Order(), new Order());

        Map<Integer, Set<Order>> collect = orders.stream()
                .collect(Collectors.groupingBy(Order::getOrderType, toSet()));

groupingBy 还提供了一个重载,让你能够自定义收集器类型,所以它的第二个参数是一个 Collector 收集器对象。

对于 Collector 类型,咱们个别还是应用 Collectors 类,这里因为咱们后面曾经应用了 Collectors,所以这里不用申明间接传入一个 toSet()办法,代表咱们将分组后的元素收集为 Set。

groupingBy 还有一个类似的办法叫做 groupingByConcurrent(),这个办法能够在并行时进步分组效率,然而它是不保障程序的,这里就不开展讲了。

3.3 分区办法

接下来我将介绍分组的另一种状况——分区,名字有点绕,但意思很简略:

将数据依照 TRUE 或者 FALSE 进行分组就叫做分区。

举个例子,咱们将一个订单汇合依照是否领取进行分组,这就是分区:

        List<Order> orders = List.of(new Order(), new Order());
        
        Map<Boolean, List<Order>> collect = orders.stream()
                .collect(Collectors.partitioningBy(Order::getIsPaid));        

因为订单是否领取只具备两种状态:已领取和未领取,这种分组形式咱们就叫做分区。

和 groupingBy 一样,它还具备一个重载办法,用来自定义收集器类型:

        List<Order> orders = List.of(new Order(), new Order());

        Map<Boolean, Set<Order>> collect = orders.stream()
                .collect(Collectors.partitioningBy(Order::getIsPaid, toSet()));

3.4 经典复刻办法

终于来到最初一节了,请原谅我给这部分的办法起了一个这么土的名字,然而这些办法的确如我所说:经典复刻。

换言之,就是 Collectors 把 Stream 原先的办法又实现了一遍,包含:

  1. mapmapping
  2. filterfiltering
  3. flatMapflatMapping
  4. countcounting
  5. reducereducing
  6. maxmaxBy
  7. min minBy

这些办法的性能我就不一一列举了,之前的文章曾经讲的很详尽了,惟一的不同是某些办法多了一个参数,这个参数就是咱们在分组和分区外面讲过的收集参数,你能够指定收集到什么容器内。

我把它们抽出来次要想说的为什么要复刻这么多办法解决,这里我说说个人见解,不代表官网意见。

我感觉次要是为了性能的组合。

什么意思呢?比方说我又有一个需要:应用订单类型对订单进行分组,并找出每组有多少个订单。

订单分组咱们曾经讲过了,找到其每组有多少订单只有拿到对应 list 的 size 就行了,然而咱们能够不这么麻烦,而是一步到位,在输入后果的时候键值对就是订单类型和订单数量:

        Map<Integer, Long> collect = orders.stream()
                .collect(Collectors.groupingBy(Order::getOrderType, counting()));

就这样,就这么简略,就好了,这里等于说咱们对分组后的数据又进行了一次计数操作。

下面的这个例子可能不对显著,当咱们须要对最初收集之后的数据在进行操作时,个别咱们须要从新将其转换成 Stream 而后操作,然而应用 Collectors 的这些办法就能够让你很不便的在 Collectors 中进行数据的解决。

再举个例子,还是通过订单类型对订单进行分组,然而呢,咱们想要拿到每种类型订单金额最大的那个,那么咱们就能够这样:

        List<Order> orders = List.of(new Order(), new Order());        
       
        Map<Integer, Optional<Order>> collect2 = orders.stream()
                .collect(groupingBy(Order::getOrderType, 
                        maxBy(Comparator.comparing(Order::getMoney))));

更简洁,也更不便,不须要咱们分组完之后再去一一寻找最大值了,能够一步到位。

再来一个分组之后,求各组订单金额之后的:

        List<Order> orders = List.of(new Order(), new Order());        
       
        Map<Integer, Long> collect = orders.stream()
                .collect(groupingBy(Order::getOrderType, summingLong(Order::getMoney)));

不过 summingLong 这里咱们没有讲,它就是一个内置的请和操作,反对 Integer、Long 和 Double。

还有一个相似的办法叫做 averagingLong 看名字就晓得,求均匀的,都比较简单,倡议大家没事的时候能够扫两眼。


该完结了,最初一个办法 joining(),用来拼接字符串很实用:

        List<Order> orders = List.of(new Order(), new Order());

        String collect = orders.stream()
                .map(Order::getOrderNo).collect(Collectors.joining(","));

这个办法的办法名看着有点眼生,没错,String 类在 JDK8 之后新加了一个join() 办法,也是用来拼接字符串的,Collectors 的 joining 不过和它性能一样,底层实现也一样,都用了 StringJoiner 类。

4. 总结

终于写完了。

在这篇 Stream 中终结操作中,我提了 Stream 中的所有聚合办法,能够说你看完了这篇,Stream 的所有聚合操作就把握个七七八八了,不会用没关系,就晓得有这个货色了就行了,不然在你的常识体系中 Stream 基本做不了 XX 事,就有点贻笑大方了。

当然,我还是倡议大家在我的项目中多多用用这些简练的 API,晋升代码可读性,也更加简练,被 review 的时候也容易让他人眼前一亮~


参考书籍:

  • Java8 实战
正文完
 0