Java 8 stream包初步认识

是什么?java

这确定是一个咱们最想先了解的问题.git

java.util.stream包是Java 8的新特性之一,虽然名字乍一看好像和io包中的InputStream,OutputStream等很相似,但做用是彻底不一样的.它的做用是专一于Collection对象进行各类很是便利,高效的聚合操做或者大批量数据操做,支持同为Java 8新特性的Lambda表达式(可参见另外两篇Lambda的技术分享文章)以及方法引用(后面使用时一块儿介绍),极大的提升编程的效率和程序可读性.而且提供串行执行和并行执行两种方式进行集合对象的处理,底层采用了Java 7中引入的fork/join框架.这意味着咱们不用去写一行多线程代码,就可充分利用多核的性能处理集合对象.而多线程代码的编写,偏偏是不容易把握的,须要咱们十分谨慎才不至于弄巧成拙,Stream的出现无疑是一个好事.github

简而言之,两个字,厉害.数据库

说了一些概念上的东西,想必有些”太长不看”的朋友会对代码更感兴趣,那么咱们来写一点代码,做为Stream的简单示例:编程

假设咱们有一个集合,集合中的元素是订单(Order)对象,每一个订单包含一个金额(amount)以及一个性别(gender),需求是须要把集合中每一个性别为男(true)的而且金额大于50000的订单按照金额从小到大排序,并将最终结果打印出来,按照Java 7的for循环写法,大体应该是这样:数组

(for循环写法太长以致于直接在IDEA中截图都不方便截完,因此放到了笔记中再次截图.)多线程

而若是咱们使用Java 8的Stream呢,大概应该是这样写:app

上图的示例中,咱们使用到了Stream API,使用到了Lambda表达式,同时也使用到了方法引用.有没有感受清爽了不少,没有那么多层做用域,代码量与可读性都大大提升.框架

固然,咱们也能够直接从数据库中使用各类函数及条件,将数据过滤再返回,但这个就是数据库层面的操做,咱们在此处只区别Java代码中的区别,就不要在乎这些细节了.dom

经过for循环方式的代码能够看到,咱们先遍历了一次集合,将符合gender为true且amount>50000的订单对象取出,放入到一个新的集合中,而后再对集合排序,虽然Java 8已经对底层进行了优化,但排序仍然是再次遍历了一遍,因此是遍历了两遍.

而stream难道说有什么不一样吗?是的,stream只遍历了一次.虽然咱们调用了filter方法,调用了sorted方法,最终调用collect方法生成了一个List,但并非每调用一个方法就会当即执行操做.这里咱们引入两个概念:

l Intermediate(中间操做):一个流能够后面跟随零个或多个intermediate操做。其目的主要是打开流,作出某种程度的数据映射/过滤,而后返回一个新的流,交给下一个操做使用。这类操做都是惰性化的(lazy),就是说,仅仅调用到这类方法,并无真正开始流的遍历。调用中间操做只会返回一个新的Stream对象.

l Terminal(终端操做):一个流只能有一个terminal操做,当这个操做执行后,流就被消耗掉,没法再被操做。因此这一定是流的最后一个操做。Terminal操做的执行,才会真正开始流的遍历,而且会生成一个结果,或者一个side effect。Side effect在这里能够理解为咱们操做引用对象所形成的结果,虽然没有返回新的值,但被操做的引用对象可能已经被修改了.调用终端返回的是非Stream对象或者无返回值.

是否是已经感受到了Stream API并不只仅只是对for循环的简单增强,而是从底层换了另一种方式去实现.这意味着咱们加上再多的Intermediate操做,也只会在执行Terminal操做的时候才开始遍历,这样比传统的for循环在性能上提高了太多.

怎么用?

咱们须要先明确一件事,那就是stream操做并非直接操做集合,而是将集合中的元素复制到了一个流中,对流的操做不会影响到原来的集合.

对于如何使用,咱们第一步应该是获取到一个Stream对象,而后再根据文档来操做Stream对象.通常来讲,最经常使用的一种获取方式就是如上面示例的方式,调用集合对象的stream方法.

对于数组,咱们可使用Arrays.stream(数组)的方式,将须要得到流的数组传入,获得一个流.

以上两种方法对咱们的基本使用来讲已经足够.

这里咱们说一说方法引用,简单理解来讲,就是把方法当作参数传入,而后咱们对流中的每个元素都执行咱们传入的这个方法.下面的代码部分可能会用到Lambda表达式以及方法引用.

接下来咱们就须要根据需求去对数据进行处理.

经常使用的Intermediate操做有:

map,filter,distinct,sorted,peek,limit,skip,parallel

从方法名称来看咱们也大体能够知道其做用,直接经过代码示例来看怎么使用吧.

1.Map

将上游流中的元素,按照给定的逻辑进行处理,返回另外一种类型的元素,而且生成一个新的流,交给后面的操做处理.

能够看到示例中,咱们调用了map操做,而且经过方法引用的形式传入了一个参数,最终生成了一个Stream<Integer>对象.在上面代码中,咱们从randomList中获取了一个流,而后对流中的每个元素执行了getAmount方法,获得了一个Integer对象的流.这只是一个简单的使用,一样咱们也能够执行一些比较复杂的操做,好比Order中有name字段,咱们把name做为key,amount做为value,将每一个Order对象转换为一个Map对象,并生成一个Map的流,这些都是能够的,你们能够本身尝试一下.

map有一些特殊方法.正如咱们所知,装箱拆箱其实是比较消耗性能的,因此Stream提供了mapToInt,mapToLong,mapToDouble三种方法,用于操做int,long和double,以便于后面的操做可以在性能上获得优化.

map也有一个flatMap方法,能够将流中的集合或数组扁平化,例如二维数组,按照通常的map咱们获得的流中的元素就是一个一个的数组,这可能不怎么符合咱们一些场景的指望.当咱们须要得到确切的每个最底层元素时,咱们可使用flatMap,将原先的结构扁平化.代码示例以下:

咱们建立了3个String数组,而后将3个String数组又放入到了一个数组中,由此咱们获得了一个二维数组.

接下来咱们得到了二维数组的流,经过flatMap方法,将每个流中的元素执行了获取流的操做,而获取到的就是元素为String的流.最终将二维数组的流转换成为了一个其底层元素的流.到这里咱们再简单的调用一个collect方法,就能够生成一个集合.

一样的,flatMap也有三个用于int,long和double的方法,即为flatMapToInt,flatMapToLong和flatMapToDouble.

2.Filter

见名知意,用于将上游流中的元素按照咱们给定规则进行过滤的操做.

在最上面的示例中,咱们以Lambda表达式的形式传入了咱们所指定的过滤规则

刚才演示map操做的时候,都使用的是方法引用的方式,这里由于判断条件须要调用两个方法,将两个布尔值(布尔表达式)组合为一个布尔表达式,因此咱们用到了Lambda表达式.

怎么理解这个表达式呢?

咱们表达式中在箭头符号左边写了一个e,这个e是咱们本身定义的一个变量,改为别的什么其实没差异,它指代的是咱们流中的每个当前元素,能够直观的理解为咱们在遍历这个集合,而每一个当前元素就是咱们的e,而后返回的值就是箭头符号右边的语句的结果.这么一来咱们这个filter的规则就是:”将每个流中的元素,判断其gender值是否为true且amount大于50000”.filter操做期待的返回值是一个布尔值,为true则代表当前元素符合规则,放入下游流,若是为false则不符合,继续判断下一个.

固然,我这里所写的Lambda表达式只是最简单的一种状况,其余的状况能够参见另外两篇文章详细了解.

3.Distinct

   眼熟吗?是的,就是同一个意思,去重.

这个操做不须要参数,根据Object.equals方法进行判断.使用起来很简单,因此就不额外写代码演示了,你们能够本身尝试一下.

4.Sorted

上面咱们示例中也是用到了的,其实它重载了一个无参的sorted方法,无参的就是按照天然排序来将流中的元素进行排序,但这只能用于实现了Comparable接口的元素.一样咱们能够用上图示例中的方式,传入一个自定义的Comparator来实现排序,而咱们所用的方式是Java 8新增的一种方式,简单理解来讲就是根据咱们传入的方法(获得用于肯定排序依据的字段),获得一个Comparator.

上图即拆开写的排序,能够看到实际上咱们sorted方法接收的就是一个Comparator

5.Limit/skip

limit只能传入一个参数,即指定最多返回多少个元素.

而skip传入一个参数,则表示跳过前面多少个元素.

这两个使用起来也是很简单的,也就不写代码作演示了.

6.Peek

这个操做在我第一次看到的时候是以为难以理解的,主要在于它的使用方法以及返回都和map差很少,对每个传入的元素进行操做,而后获得一个流.但后面了解了forEach这个Terminal操做以后,发现它其实功能与forEach很像,但差异就在于peek处理以后还会生成一个与上游流元素类型相同的下游流,而forEach做为一个Terminal操做,会将流消耗掉.

看看源码注释中的示例:

除了得到流的方式不一样之外,其他的操做也基本是前面讲到过的,在这个示例中,peek用于将每一个元素进行打印,固然咱们也能够对每一个元素作一些从新赋值之类的操做.

7.Parallel

这个方法没有任何参数,用处也就在于咱们前面提到过的,串行与并行.咱们想要得到一个并行流,要么在建立的时候调用集合的parallelStream获取,要么就直接获取一个stream而后使用parallel操做,将流转为并行流.不然咱们得到的都是默认的串行流,而串行与并行的区别能够看最后的stream的性能介绍.

经常使用的Intermediate操做就是上面所介绍的这些,还有一些其余的能够自行去查找资料了解.接下来咱们来看看有什么经常使用的Terminal操做.

1.forEach

这个名字也是很直观了,跟咱们以前经常使用的foreach差很少,就是遍历.这个foreach直接使用的话,其实和for循环没什么区别,并不神奇.只是它的参数能够支持Lambda表达式以及方法引用.

这句代码的效果其实等同于:

这么一看是否是对方法引用也有了更多的认识?

forEach还有个兄弟,forEachOrdered,名字上多了个ordered,意思就是按照顺序进行遍历,用法没什么区别.

2.toArray

这个方法应该是老方法了,能够直接不传参使用,获得一个Object数组,或是传入一个指定类型的数组,获得该类型的数组,就不赘述了.

3.Reduce

reduce这个方法,中文意思大概能够理解为”聚合”.

这个方法相比其余方法要来得复杂一些,没有那么清新.咱们看看源码中这个方法的声明:

看不懂.

看不懂.

更加看不懂了.

那么咱们直接看看怎么用的,先用起来再慢慢理解它的含义.

按照第一个方法的传值,咱们能够这么用:

咱们先经过mapToLong将每一个Order对象中的amount拿到,而后经过reduce方法去得到了amount的总和.

能够看到,reduce方法中咱们实际上传入了两个参数,一个是0,一个是Lambda表达式(e1,e2)->e1+e2.

第一个参数0能够看作咱们求总和时所写的int sum = 0,做为初始值,然后面Lambda表达式中的e1,能够看作是这个reduce方法上一次操做的结果,上一次的e1+e2结果就是这一次的e1,而e2就是当前的元素.而对于第一个元素,能够理解为它要执行操做,但没有可用的e1,就直接跳过第一次操做,把本身做为了第二次操做的e1,执行第二次操做.

这是有初始值的状况,咱们再来看看第二个:

这里我直接把初始值0去掉了,而后最终获得的结果就再也不是一个long了,而是一个OptionalLong.

这个OptionalLong是什么东西?其实它也是参照Optional而编写的一个工具类,只是Optional能够用于全部类,而OptionalLong只专一于Long类型.而Optional它是由Google的Guava工程得来,在Java 8中新增到java.util包中,用于解决空指针异常.在这里很少加赘述,感兴趣能够本身查阅资料了解更多,咱们在这里只根据咱们所用到的状况来简单说一下.

这个OptionalLong,是对结果值的一个封装,咱们能够直接调用getAsLong方法来获取到实际的结果值,但这样可能由于没有值而获得一个异常,因此咱们换一种方式来获取,避免没有值而报错

有值就返回实际结果值,没有就返回我传入的值,这样就节约了咱们写if去判断是否有值,而后再在没值的状况下写逻辑的过程.

为何会是OptionalLong?由于不一样于第一个方法,咱们没有传入一个初始值,那么万一每一个元素都是null的话,最终获得的值也是null.而第一个方法不管如何也会有个咱们传入的初始值做为保底.

而后就到第三个了,最难理解的一个.

这个方法须要区分两个场景,串行和并行,当串行时第三个参数是不会生效的.

串行时:

对于这个我已经不忍心去改成Lambda表达式了,由于改完是这样的:

好了,忘掉你看到的东西吧,咱们仍是之前面的图为例分析一下.

咱们把整个reduce的参数简化来看,就是一个orders,一个BiFunction对象,一个BinaryOperator对象.在串行时咱们用不到第三个对象,因此new出来覆写的时候随便写写吧.

第一个orders,就是初始值.但和咱们第一个方法不一样,它竟然是个List,而不是相同的Long.

没错,第三种写法能够用任意类型的初始值,而使用的方法也就在第二个参数中实现.在上面的例子中,咱们用一个List做为初始值,在new BiFunction对象覆写其apply方法时,有两个形参,一个是List,一个是Order.在执行时,list就是咱们前面传入的初始值list,而order就是遍历的当前元素,在这里咱们只是简单的将order放入了list中,你也能够根据本身的需求作其余的操做.若是初始值是同类型的,那写法就跟咱们第一种写法同样了.因此,第二个参数就是负责咱们怎么将流中元素与传入的初始值进行处理.

并行时:

我对第三个参数的apply方法逻辑进行了修改,将传入的orders2集合全部元素彻底放入orders集合中.

刚才咱们说到,第三个参数在并行时才有用,是由于并行时有多个线程,每一个线程都会有一个List<Order>,最终咱们须要将全部的list合并才能获得完整的结果,因此第三个参数的做用就是这个.

有一个点须要注意,这种操做可能会出现一个问题,就是咱们初始值若是不是一个空集合,每一个并行线程都会在此基础上继续放入Order对象,那最终合并后咱们拿到的结果集合就会有多个初始值中的元素.

4.Collect

collect方法就是将流最终生成一个集合或者Map,将流中的元素”收集”起来.在最上面的示例中,咱们也用到了collect方法做为Terminal操做.

collect有两种,一种接收3个参数,supplier,accumulator,combiner.还有一种是接收一个Collector对象.咱们先来看看这个3参数的collect方法:

仍是先贴完整写法的代码:

这个完整写法其实和reduce的第三个方法有点类似的,咱们来细看一下.

第一个参数,new了一个Supplier,泛型为HashSet<Order>.Supplier意思是供应商,而我覆写了其中的get逻辑,new了一个HashSet并返回出去.这其实就是咱们提供了一个”获取初始值”的对象,和reduce类似却又不彻底相同,都是须要初始值,但reduce是直接提供,这里是提供一个生成初始值的对象.

第二个参数和第三个参数,咱们按照reduce第三个方法的思路来猜想一下,首先第二个参数,我new了一个BiConsumer,起手感受就不太好,类型都跟reduce第二个参数不同啊.不慌,看下覆写的方法是什么参数.accept方法,接收一个HashSet,一个Order,那这个HashSet应该就是经过咱们提供的Supplier生成的初始值了,而后后面的order对象应该是咱们流中的元素.

第二个参数提供的方法,与reduce同样,会被重复调用直至流中的元素所有被消耗.

第三个,类型和第二个参数同样,但接收了两个HashSet,联想到reduce,这是用于并行处理时,最终合并结果.

固然,此处咱们使用的是HashSet做为示例,其余的集合也是可使用的.

而后仍是贴一下使用方法引用的写法,多作比较对理解也有好处:

其实与上面的完整写法一比较,方法引用的写法也就没有那么难看懂了.

再说一下collect传Collector对象的方法,这个方法没什么多说的,但内容基本就在于生成Collector对象的Collectors类上.咱们先看一看Collectors有哪些方法能够用于生成Collector:

以上是用于生成集合,或者groupingBy生成Map对象.也有一些其余的方法,例如:

能够经过传入的字符将全部结果的toString值拼接起来,这个用法相信你们也是比较熟悉的.

以上分别是计数,最小值,最大值,Int求和,long求和,double求和.

可见Collectors这个类中提供了不少功能,咱们在这里也就不一一细讲,有兴趣能够下来本身了解.

那么咱们如何经过Collectors去改造咱们前面的复杂写法呢?

就这样,就能够根据咱们所选择生成的Collector来获得最终的”收集”结果了.固然,自己要从List转Set不用这么写,这只是为了举个例子,不要在乎这些细节.

5.min/max/count

这些操做是干吗的想必不用多说了吧.使用也是常规操做,min/max传入一个Comparator,count不须要参数.

6.anyMatch/allMatch/noneMatch/findFirst/findAny

要把他们放一块儿说,是由于他们有一点点特殊.咱们都知道”&&”还有”||”运算符,他们都有短路的效果,以上方法也是一样的,一旦符合短路条件就不会遍历后面的元素.

anyMatch:任一符合给定判断条件就短路,返回一个true,

allMatch:所有符合才返回一个true,一旦有一个不符合就短路

noneMatch:所有不符合才返回一个true,一旦有一个符合就短路

findFirst:找到符合条件的第一个元素就短路,立刻返回

findAny:找到符合条件的任意一个就短路,立刻返回.

好了,至此Stream中的经常使用方法也大体介绍了一下.接下来是关于Stream性能的讨论.

stream的性能

在我最开始查找资料了解Stream性能的时候,看到一个文章

看到标题的时候内心是咯噔一下的,用起来这么爽(这么装逼)的一个东西,竟然性能比for-loop差了不止一点半点,而是相差5倍.

而后那几天是一点都不开心的,直到下面的评论多起来,以及gitHub上面一个更为可靠的测试出现,客观的评价了stream的性能.

的确,stream并非无脑高性能,但也不至于慢5倍.

先贴上gitHub的帖子地址:(中文的,不要怕)

https://github.com/CarpenterLee/JavaLambdaInternals/blob/master/8-Stream%20Performance.md

固然,对于”太长不看”的朋友,我这里大体总结一下:

1.对于简单操做,例如简单遍历,for-loop的性能高于串行stream,但并行stream会由于CPU核数提升而性能也随之提升.

2.对于复杂操做,例如map,filter一系列的操做,串行stream能够和高质量的for-loop性能至关,而此时并行stream由于CPU核数以及数据量的提升而性能碾压for-loop

3.不建议在单核环境下使用并行stream.

4.使用stream能够在底层代码有所优化时,咱们无须做出任何调整,这也是”依赖接口而不依赖实现”的优点所在

5.尽可能消除掉自动装拆箱,这样在后续的大量操做中可以节省很多的时间.而消除能够经过mapToInt,mapToLong和mapToDouble这些操做来实现.

相关文章
相关标签/搜索