本文为掘金社区首发签约文章,未获受权禁止转载。java
思惟导图镇楼,先感谢你们对我上一篇文的积极点赞,助我完成KPI😄。web
上一篇中给你们讲了Stream的前半部分知识——包括对Stream的总体概览及Stream的建立和Stream的转换流操做,并对Stream一些内部优化点作了简明的说明。编程
虽迟但到,今天就来继续给你们更Stream第二部分知识——终结操做,因为这部分的API内容繁多且复杂,因此我单开一篇给你们细细讲讲,个人文章很长,请你们忍耐一下。安全
正式开始以前,咱们先来讲说聚合方法自己的特性(接下来我将用聚合方法代指终结操做中的方法):markdown
聚合方法表明着整个流计算的最终结果,因此它的返回值都不是Stream。多线程
聚合方法返回值可能为空,好比filter没有匹配到的状况,JDK8中用Optional来规避NPE。并发
聚合方法都会调用evaluate方法,这是一个内部方法,看源码的过程当中能够用它来断定一个方法是否是聚合方法。app
ok,知晓了聚合方法的特性,我为了便于理解,又将聚合方法分为几大类:ide
其中简单聚合方法我会简单讲解,其它则会着重讲解,尤为是收集器,它能作的实在太多了。。。svg
Stream的聚合方法是咱们在使用Stream中的必用操做,认真学习本篇,不说立刻就能对Stream驾轻就熟,起码也能够行云流水吧😄
第一节嘛,先来点简单的。
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了,因此它们都是具备短路效果的方法。
第二节咱们来讲说归约,因为这个词过于抽象,我不得不找了一句通俗易懂的解释来翻译这句话,下面是归约的定义:
将一个Stream中的全部元素反复结合起来,获得一个结果,这样的操做被称为归约。
注:在函数式编程中,这叫作折叠( fold )。
举个很简单的例子,我有一、二、3三个元素,我把它们俩俩相加,最后得出6这个数字,这个过程就是归约。
再好比,我有一、二、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,那么你的最终和就会比你预想的结果要大。
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的封装。
min仍是直接看例子吧:
Optional<Integer> max = List.of(1, 2, 3).stream()
.min(Integer::compare);
复制代码
它和max区别就是底层把 >
换成了 <
,过于简单,再也不赘述。
第三节咱们来看看收集器,它的做用是对Stream中的元素进行收集而造成一个新的集合。
虽然我在本篇开头的时候已经给过一张思惟导图了,可是因为收集器的API比较多因此我又画了一张,算是对开头那张的补充:
收集器的方法名是collect,它的方法定义以下:
<R, A> R collect(Collector<? super T, A, R> collector);
复制代码
顾名思义,收集器是用来收集Stream的元素的,最后收集成什么咱们能够自定义,可是咱们通常不须要本身写,由于JDK内置了一个Collector的实现类——Collectors。
经过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() 具备两个参数:
第一个参数表明key,它表示你要设置一个Map的key,我这里指定的是元素中的orderNo。
第二个参数表明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是相同的。
若是你想对数据进行分类,可是你指定的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(),这个方法能够在并行时提升分组效率,可是它是不保证顺序的,这里就不展开讲了。
接下来我将介绍分组的另外一种状况——分区,名字有点绕,但意思很简单:
将数据按照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()));
复制代码
终于来到最后一节了,请原谅我给这部分的方法起了一个这么土的名字,可是这些方法确实如我所说:经典复刻。
换言之,就是Collectors把Stream原先的方法又实现了一遍,包括:
map → mapping
filter → filtering
flatMap → flatMapping
count → counting
reduce → reducing
max → maxBy
**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类。
终于写完了。
在这篇Stream中终结操做中,我提了Stream中的全部聚合方法,能够说你看完了这篇,Stream的全部聚合操做就掌握个七七八八了,不会用不要紧,就知道有这个东西了就好了,否则在你的知识体系中Stream根本作不了XX事,就有点贻笑大方了。
固然,我仍是建议你们在项目中多多用用这些简练的API,提高代码可读性,也更加简练,被review的时候也容易让别人眼前一亮~
看到这的掘友,但愿高抬贵手帮我点个赞,为个人掘金KPI大业出一份力,大家的支持就是我创做的不竭动力,咱们下期见。
参考书籍:
推荐文章: