在上篇文章中介绍了Java8引入的Lambda表达式的基本概念,现在至少应该知道行为参数化、函数式接口、方法符号描述符等概念的含义,这属于理解本节内容的前置条件。
landexiang:Java8(一)Lambda表达式zhuanlan.zhihu.com一、集合和流
集合是JDK提供的最基本的数据结构类型,几乎没有功能可以不借助List、Set、Map来完成。如果说Java中的集合是位于内存中的数据集,是一种静态的概念。而流可以表示为数据的状态流转过程,是一个动态的概念,举个实际的例子:
假如将仓库中的所有瓶装香水看做一个集合,则将瓶装香水包装成合格的可合法出售的商品这整个过程可以看做一个流
Java8中,提供了流这个抽象概念的具体实现——Stream。最常见的流创建方式是从数据源中创建,数据源可以是数组、也可以是集合。
- 数组创建流
- 集合创建流
二、流的使用
如果将瓶装香水包装成合格的可合法出售的商品这一整个过程看做一个流,那么在这整个流中可能会遇到各种各样的处理过程,比如给香水贴标签、检验香水质量、按香水款式分类、按原产地分类....下面通过实际的例子,我们来看下Java是如何定义这些流中的处理过程的。首先创建一个香水的商品包装流:
仔细思考其实不难发现,流水线上的处理过程可以分为两种
- 标志整个处理流程已经完成,不需要后续操作,比如统计数量、将商品装箱
- 属于整个处理流程中的一环,仍然需要后续操作,比如贴标签、除去质量差的、分类等
在Java的Stream中,这两种处理过程分别被定义为中间操作和终端操作。从代码的角度来看,所谓的处理流程都是Stream提供的API:
那Java作者是根据什么来区分这些API哪些是中间操作,哪些是终端操作呢?答案就是返回值——所有返回值为Stream类型的操作都属于中间操作,所有返回值不是Stream类型的操作都是终端操作。下面结合实例,来分析一下Stream中都有哪些常用的中间操作和常用的终端操作。
(一) 常见的中间操作
- 筛选操作(filter)——选出价格大于500的香水
Stream中的filter方法,其入参为Predicate(关于函数式接口和Lambda的内容,请点击开头的文章链接)。所以只需要给Stream的filter方法传递一个判定条件,就可以起到对流中元素进行筛选的作用,所以代码应该写成如下形式:
- 分片操作(limit、skip)——选出从第5瓶香水开始的10瓶香水抽样质检
Stream的limit和skip方法的参数都为long类型,即限定流中处理元素的个数和跳过流开头元素的个数,所以代码应该写成如下形式:
- 映射操作(map)——统计所有香水的价格集合
Stream的map方法,接受一个Function类型的参数,Function函数式接口的方法描述符为
T -> R
从方法描述符就可以看出,这个映射其实就是convert的操作,不过不知道为什么Java作者将其命名为map,因此为了统计所有香水的价格集合,代码可以写成如下形式:
这里需要注意的是经过map操作后,Stream的元素类型发生了变化
- 匹配操作(anyMatch、allMatch、noneMatch)——检测香水价格是否标错
在取得所有香水的标价之后,需要在流上增加一步来判断香水的标价是否正确,这个时候就需要对整个香水的价格区间进行检测。使用anyMatch、allMatch、noneMatch都可以完成该动作。
这三个方法的入参都为Predicate,即我们只需要传入一个条件表达式即可。
anyMatch:找到任意一个满足Predicate的元素即停止,并且返回true,否则返回false
allMatch:找到任意一个不满足Predicate的元素即停止,并且返回false,否则返回true
noneMatch:找到任意一个满足Predicate的元素即停止,并且返回false,否则返回true
假设香水的正确价格区间为[500, 1000],那么如何利用上述三种匹配操作检验流中所有香水的标价都正确呢?
- 排序操作(sorted)——将所有的香水按照价格排序
如果我们需要将流中的所有元素按照某种顺序进行排序,则可以借助sorted操作。
Stream.sorted方法的参数为一个Comparator函数式接口
其方法描述符为,用于在两个数值之间进行比较
(T, T) -> int
所以该功能的实现如下:
- 去重操作(distinct)——统计所有香水的原产地,只需要统计一次
如果我们需要统计流中的元素去重后的结果,可以借助distinct。
在使用distinct时不需要传递任何参数,JDK提供给我们默认的实现,所以统计所有香水的原产地的功能实现如下:
(二)常见的终端操作
- 查找操作(findFirst、findAny)——找到第一个 / 任意一个标价错误的香水
如果我们为了追踪数据的源头,想找到流水线上第一个标价错误的香水,或者说流水线上任意一个标价错误的香水,如果用Java来表示,这两个操作分别对应于findFirst和findAny。
findFirst和findAny在单线程下并没有什么不同,都是返回流中的第一个元素。那么为什么还需要findAny呢?答案是如果在并行的场景下,findFirst的值和findAny的值可能会有不同。
另外观察Stream接口中对于这两个方法的定义,其返回值并不是T,而是Optional<T>:
Optional的设计理念是为了统一对于Null值的处理。其基本用法如下:
每个Optional都是对于一个Stream操作结果的封装。通过调用Optional的isPresent方法可以判断操作结果是否为空,ifPresent方法可以传入一个符合Comsumer函数式接口的lambda表达式,表示如果操作结果不为空的情况下,可以直接应用该lambda表达式对于结果进行处理。而如果要进行更复杂的结果处理,则必须借助于isPresent显式判断操作结果,并且进行处理。
- 归约操作(reduce)——计算所有香水的价格总和
根据统计需要,我们需要计算流中的所有香水的价格总和,这个时候可以借助于归约操作reduce来完成功能。
在Stream中,reduce一共有两种不同的方法定义,一种是带有初始值的,一种是不带初始值的。
BinaryOperator函数式接口继承于函数式接口BiFunction,所以其唯一的抽象方法同样是在BiFunction中定义的
只不过在BinaryOperator的定义中,T、R、U三个泛型化参数的类型相同
即accumulator的函数签名为
(T, T) -> T
即reduce的实际意义就是将stream中的每一个元素与上一次计算的结果,再次进行计算并返回,如果有初始值则第一次参与计算的两个元素是初始值和流中的第一个元素,如果没有初始值则第一次参与计算的两个元素是流中的第一个元素和第二个元素。
由于Stream可能为空,所以在没有初始值的情况下,按照Stream的设计理念,需要返回一个Optional对象,而有初始值的情况下可以直接返回计算结果。因此,计算所有香水总价格的代码应该如下:
当然还有很多其他的中间、终端流操作,这里就不一一介绍了,我们可以用到的时候自己去查表。
三、中间操作的特殊性
由于中间操作的结果仍然是返回一个Stream,则一个显而易见的结论就是,多个中间操作可以连续调用,从而形成一种流式的结构,比如下图所示:
表面上看起来并没有什么特殊的,但我们可以思考一个问题:这些中间操作的执行是顺序的吗?
比如filter和map可能有两种执行顺序,如果以传统的迭代模式来解释的话如下面所示:
- filter先执行完,获得结果集之后再执行map
这个执行方式中,需要创建一个filterResult这个临时变量
- filter中的一个元素执行完,立马执行map
在这种执行模式下,不需要给filter的执行结果创建一个额外的临时变量用来保存
要想获得结论可以来对流式调用做一下调整,如下所示:
通过实验结果我们可以很清楚的看到,sorted是需要运行完之后才会继续往后面走,而对于filter和map来说,当filter为true时直接走到map,而不必等待filter全部执行完才继续执行map。也就是说,在sorted和后续的流操作中,像存在了一层水坝,只有等sorted执行完之后才会开闸放水;而filter和map之间就像是联通的,filter的一条结果执行成功之后立马流转到map,看起来就像在同一个函数中进行了两个不同的if else处理。
通常sorted这种中间操作被称为有界中间操作或者有状态中间操作,而对于filter和map这种中间操作被称为无界中间操作或者是无状态中间操作。
四、终端操作的特殊性
和中间操作的特殊性相比较而言,对于终端操作我们只需要注意一个特殊性,那就是在流式调用中,如果中间操作没有以终端操作结尾,那么整个流是“不会流动”的,如下图所示,我们将最后一个终端操作forEach注释掉,然后运行test测试用例:
结果让人惊讶,并没有任何数据被打印出来:
所谓的不会流动就是在整个流式处理中的所有中间操作,压根就不会执行。