Java8 Lambda表达式的特快处理流Stream快速入门

Stream

什么是Stream?

Java8最值得学习的特性就是Lambda表达式和Stream API,如果有python或者javascript的语言基础,对理解Lambda表达式有很大帮助,因为Java正在将自己变的更高(Sha)级(Gua),更人性化。--------可以这么说lambda表达式其实就是实现SAM接口的语法糖。

Java8中,Collection新增了两个流方法。分别是Stream()和parallelStream()

Java8中添加了一个新的接口类Stream,相当于高级的Iteratpr。它可以通过Lambda表达式对集合进行大批量数据操作。或者各种非常便利,高效的聚合数据操作。

为什么要使用Stream?

总结一下本人最常用的场景。在工作中我们经常需要操作数据库。往往会出现一个方法中就需要对一个表的数据进行多次访问。例如我一个方法要做三次数据库操作。一次要计算总条数(count),一次要查询数据(select *),一次要对某些数据查询并进行累加(sum)等等。

三次都操作数据库不是不可以。但是过于频繁的访问数据库,无疑会增加网络传输的消耗,也会增加MySQL服务器的压力。那么,我们的目就转移到了,如何在减少与数据库访问的情况下完成这些业务逻辑。

此时,我们可以用将要查询的数据全部从数据库中获取到一个集合中,在这个集合中,根据不同的条件,过滤出我们想要的数据。如何高效的用代码在集合中过滤出我们想要的数据呢?

Java8之前,我们通常是通过for循环或者Iterator迭代来重排序合并数据,又或者通过重新定义Collections.sorts的Comparator方法来实现。这两种方式对于大数据量系统来说,效率并不是很理想。

Stream的聚合操作与数据库SQL的聚合操作sorted,filter,map等类似。我们在应用层就可以高效地实现类似数据库SQL的聚合操作了。而在数据操作方面,Stream不仅可以通过串行的方式实现数据操作,还可以通过并行的方式处理大批量数据,提高数据的处理效率。

Stream简单例子之筛选查询

如果我们需要从一个list中根据某个条件筛选查询,不使用Stream的方式如下:

image.png

需要6行代码完成。

使用Stream改进后:

image.png

Stream操作API

官方将Stream中的操作分为两大类:终结操作和中间操作。

中间操作会返回一个新的流,一个流可以后面跟随零个或多个中间操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个流操作使用。这类操作都是惰性化的(lazy),也就是说,仅仅调用到这个类,还没有开始流的遍历(其实就是给接下来的终结操作先加入条件)。而真正的流遍历是在终结操作开始的时候才真正开始执行。

中间操作又可以分为无状态(Stateless)与有状态(Stateful)操作。无状态是指元素的处理不受之前元素的影响。有状态是指该操作需要在之前的操作的基础上继续执行。

终结操作是指返回最终的结果。一个流只能有一个终结操作。当这个操作执行后,这个流就被用”光”了,无法再被操作。所以这必定是这个流的最后一个操作。终结操作才会开始进行流的遍历,并且生成结果。

终结操作又可以分为短路与非短路。

短路是指遇到某些符合条件的元素就可以得到最终结果。

非短路是指必须处理完所有元素才能得到最终结果。操作分类详情如下:

图片8.png

因为Stream操作类型非常多,总结一下常用的:

1. mapToXXX():将流中的原本类型的元素挨个加工变为XXX类型元素,常搭配sum()使用。

2. filter():对流中的元素进行遍历筛选,流下符合条件的数据组成新的流。

3. limit():返回指定数量的流元素。返回的是Stream里前n个元素。

4. skip():将指定数量的元素从流程排除,剩下的元素组成新的流并返回。

5. sorted():将流中的元素按自然排序进行排序。

6. distinct():将流中的元素去重后输出。

7. map():将流中的元素进行再次加工形成一个新的流(常用的有整个流留的小写转大写,以及List<Object>转其中某个元素List<Integer>

8. peek():与map类似,但与map的区别是它相当于在操作的时候生成一个新的流,并且该操作不会影响到原本流的执行结果。因此基本用于debug。

9. collect():就整个流进行集合转换(转为list,set,map等)

Stream的底层实现

Stream操作叠加

一个Stream的各个操作是由处理管道组装。并统一完成数据处理的。

我们知道Stream有中间操作和终结操作,那么对于一个写好的Stream处理代码来说,中间操作是通过AbstractPipeline生成了一个中间操作Sink链表。当我们调用终结操作时,会生成一个最终的ReducingSink。通过这个ReducingSink触发之前的中间操作,从最后一个ReducingSink开始,递归产生一个Sink链。因此说Stream是惰性化的。如下图所示:

图片9.png

Stream的peek()和map()的区别

刚开始使用Stream的时候,看定义没懂peek是什么意思。看代码感觉用法和map很像。那么二者之间的区别是什么呢?

现有如下代码:

图片12.png

可以看到我们map()在执行打印时编译会报错,这是为什么呢?

图片10.png

从peek方法中,我们看到形参是Consumer。Consumer是没有返回值的,它只是对Stream中的元素进行某些操作。但是操作之后并不会影响整个流的数据。因此后续打印返回的依旧是原来的元素。

图片13.png

可以看到map方法中,形参是Function。Function是返回值的。所以经过map中间操作的流都会收到该操作影响。

而又由于它们各自的特性,打印操作这种无法返回值的就交给peek来处理。而大小写转换这种操作就交给map来处理。

图片11.png

因此,我们常常使用peek作为中间操作的”debug”。

Stream的其它案例

现有一个List

图片15.png

按性别分组

图片16.png

按身高过滤

图片17.png

按身高求和

图片19.png

按身高找最大最小值

图片18.png

Stream的性能

需求

我们写三个方法,寻找list的最小值。来对比他们的执行效率。

常规迭代

图片20.png

串行Stream

  • 🔲图片21.png

并行Stream

图片22.png

list中100个元素效率对比

图片23.png

图片24.png

解释原因

1. 常规的迭代代码简单,越简单的代码执行效率越高。

2. Stream串行迭代,使用了复杂设计,导致执行效率低。所以性能最低。

3. Stream并行迭代,使用了Fork-Join线程池,所以效率比Stream串行高。但还是比常规迭代慢。

list一个亿元素(使用默认CPU核心数)

 

图片25.png

图片27.png

解释原因

1. Stream 并行迭代 使用了 Fork-Join 线程池, 而线程池线程数为 cpu 的核心数(我

的电脑为 12 核),大数据场景下,能够利用多线程机制,所以效率比 Stream 串行迭代快,同时多线程机制切换带来的开销相对来说还不算多,所以对比常规迭代还是要快(虽然设计和代码复杂)

2. 常规迭代代码简单,越简单的代码执行效率越高。

3. Stream 串行迭代,使用了复杂的设计,导致执行速度偏低。所以是性能最低的。

list一个亿元素(使用默认CPU=2)

图片28.png

图片29.png

解释原因

Stream 并行迭代 使用了 Fork-Join 线程池,大数据场景下,虽然利用多线程机制,但是线程池线程数为 2,我们的Forkjoin体现的分而治之的思想,将任务划分为多份。如果线程数只有2个,任务数大于CPU核心数,就会发生任务对CPU资源的争夺(2个争抢的太厉害了)。所以对比常规迭代还是要慢(虽然用到了多线程技术)

list一个亿元素(使用默认CPU=240)

图片30.png

图片31.png

解释原因

Stream 并行迭代 使用了 Fork-Join 线程池, 而线程池线程数为 240,大数据场景下,虽然利用多线程机制,但是线程太多,线程的上下文切换成本过高,所以导致了执行效率反而没有常规迭代快。

Spliterator与Iterator的性能对比

此二者都是集合遍历器。在jdk1.8版本后出现了Spliterator。

二者的区别就是一个Iterator是顺序遍历,而Spliterator是并行遍历,常常搭配Stream使用,也就是我们上述案例。这两个迭代器的性能往往由底层决定。和机器CPU性能,核心数密不可分。在生产环境下我们要想使用更加合适高效的迭代器,往往需要我们实际压测得出最终方案。

如何合理使用 Stream?

我们可以看到:在循环迭代次数较少的情况下,常规的迭代方式性能反而更好;而在大数据循环迭代中, parallelStream(合理的线程池数上)有一定的优势。

但是由于所有使用并行流 parallelStream 的地方都是使用同一个 Fork-Join 线程池(当并行的Stream操作变多时,这个设置很难控制),而线程池线程数仅为 cpu 的核心数。切记,如果对底层不太熟悉的话请不要乱用并行流 parallerStream(尤其是你的服务器核心数比较少的情况下)。

另外如果对线程池CPU核心数配置感兴趣的朋友,可以了解一下CPU密集型数据以及IO密集型数据下线程池的创建。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

大将黄猿

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

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

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

打赏作者

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

抵扣说明:

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

余额充值