思维导图镇楼。

上一篇中给大家讲了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实战