
酷炫的 Stream API 最佳指南
Java 8 带来一大新特性 Lambda 表达式流(Stream),当流与 Lambda 表达式结合使用,代码将变得相当骚气与简洁。
超级大招,释放代码
假如有一个需求,需要对数据库查询的发票信息进行处理:
- 取出金额小于 10000 的发票。
- 对筛选出来的数据排序。
- 获取排序后的发票销方名称。
发票 Model
我们使用传统的方式实现,在之前我们初始化测试数据
Java8 之前的实现方式
Java8 之后的骚气操作,一气呵成。再也不用加班写又臭又长的代码了
一套龙服务的感觉,一气呵成送你上青天。大大减少了代码量。
现在又来一个需求
对查询出来的发票数据进行分类,返回一个 Map<Integer, List> 的数据。
回顾下 Java7 的写法,有没有一种我擦,这也太麻烦了。还能不能早点下班回去抱女朋友。
接着就是我们利用 stream 的骚操作代码实现上面的需求
groupingBy 分组
就是这么简单粗暴,一行代码直捣黄龙。
什么是 Stream?
Stream(流)是一个来自数据源的元素队列并支持聚合操作,它不是数据结构并不保存数据,主要目的是在于计算。
元素是特定类型的对象,形成一个队列。Java中的Stream并不会存储元素,而是按需计算。数据源流的来源。可以是集合,数组,I/O channel, 产生器 generator 等。聚合操作类似SQL语句一样的操作,比如filter, map, reduce, find, match, sorted等。和以前的Collection操作不同,Stream操作还有两个基础的特征:
● Pipelining:中间操作都会返回流对象本身。这样多个操作可以串联成一个管道,如同流式风格(fluent style)。这样做可以对操作进行优化,比如延迟执行(laziness)和短路( short-circuiting)。
● 内部迭代:以前对集合遍历都是通过Iterator或者For-Each的方式, 显式的在集合外部进行迭代,这叫做外部迭代。Stream提供了内部迭代的方式,通过访问者模式(Visitor)实现。
如何生成流
主要有五种方式
1. 通过集合生成
2.通过数组生成
通过Arrays.stream方法生成流,并且该方法生成的流是数值流【即IntStream】而不是Stream<Integer>。补充一点使用数值流可以避免计算过程中拆箱装箱,提高性能。
Stream API提供了mapToInt、mapToDouble、mapToLong三种方式将对象流【即Stream】转换成对应的数值流,同时提供了boxed方法将数值流转换为对象流
3. 通过值生成
通过Stream的of方法生成流,通过Stream的empty方法可以生成一个空流
4. 通过文件生成
通过Files.line方法得到一个流,并且得到的每个流是给定文件中的一行
5. 通过函数生成,iterate和generate两个静态方法从函数中生成流
iterator: iterate方法接受两个参数,第一个为初始化值,第二个为进行的函数操作,因为iterator生成的流为无限流,通过limit方法对流进行了截断,只生成5个偶数
generator: 接受一个参数,方法参数类型为Supplier,由它为流提供值。generate生成的流也是无限流,因此通过limit对流进行了截断
流的操作类型
主要分为两种类型
1. 中间操作
一个流可以后面跟随零个或多个中间操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。
这类操作都是惰性化的,仅仅调用到这类方法,并没有真正开始流的遍历,真正的遍历需等到终端操作时,常见的中间操作有下面即将介绍的filter、map等
2. 终端操作
一个流有且只能有一个终端操作,当这个操作执行后,流就被关闭了,无法再被操作,因此一个流只能被遍历一次,若想在遍历需要通过源数据在生成流。终端操作的执行,才会真正开始流的遍历。如下面即将介绍的 count、collect 等。
中间操作 API
filter筛选
distinct去除重复元素
limit返回指定流个数
通过limit方法指定返回流的个数,limit的参数值必须>=0,否则将会抛出异常
skip跳过流中的元素
通过skip方法跳过流中的元素,上述例子跳过前两个元素,所以打印结果为2,3,4,5,skip的参数值必须>=0,否则将会抛出异常。
map流映射
所谓流映射就是将接受的元素映射成另外一个元素
通过 map 方法可以完成映射,该例子完成中String -> Integer的映射,之前上面的例子通过 map 方法完成了 Invoice -> String 的映射
flatMap流转换
将一个流中的每个值都转换为另一个流
map(w -> w.split(" "))的返回值为Stream<String[]>,我们想获取Stream<String>,可以通过flatMap方法完成Stream ->Stream的转换。所以最后打印的结果是 [H, e, l, o, W, r, d]
元素匹配
1.allMatch匹配所有
2.anyMatch匹配其中一个
存在作废发票则打印
等同于
3.noneMatch全部不匹配
终端操作
统计流中元素个数
1.使用 count
2.使用 counting
最后一种统计元素个数的方法在与collect联合使用的时候特别有用
查找
1.findFirst查找第一个
通过 findFirst 找到金额小于 10000 的第一个元素
2.findAny随机查找一个
通过findAny方法查找到其中一个小于 10000 的元素并打印,因为内部进行优化的原因,当找到第一个满足大于三的元素时就结束,该方法结果和findFirst方法结果一样。提供findAny方法是为了更好的利用并行流,findFirst方法在并行上限制更多【本篇文章将不介绍并行流】
reduce将流中的元素组合起来
假设我们对一个集合中的值进行求和
jdk8 之前
jdk8之后通过reduce进行处理
比如统计发票金额求和
继续使用方法引用简化
reduce 接受两个参数,一个初始值这里是0,一个BinaryOperator<T> accumulator来将两个元素结合起来产生一个新值,
另外reduce方法还有一个没有初始化值的重载方法
获取流中最小最大值
通过min/max获取最小最大值
也可以写成
min获取流中最小值,max获取流中最大值,方法参数为Comparator<? super T> comparator
通过minBy/maxBy获取最小最大值
通过reduce获取最小最大值
求和
通过summingInt
如果数据类型为double、long,则通过summingDouble、summingLong方法进行求和
通过reduce
通过sum,最佳写法
在上面求和、求最大值、最小值的时候,对于相同操作有不同的方法可以选择执行。可以选择collect、reduce、min/max/sum方法,推荐使用min、max、sum方法。因为它最简洁易读,同时通过mapToInt将对象流转换为数值流,避免了装箱和拆箱操作
通过averagingInt求平均值
如果数据类型为double、long,则通过averagingDouble、averagingLong方法进行求平均
对于BigDecimal 则需要先求和再除以总条数
通过summarizingInt同时求总和、平均值、最大值、最小值
通过foreach进行元素遍历
通过joining拼接流中的元素
通过groupingBy进行分组
在collect方法中传入groupingBy进行分组,其中groupingBy的方法参数为分类函数。还可以通过嵌套使用groupingBy进行多级分类
首先根据 发票类型分组,再根据开票金额大小分组,返回的数据类型是 Map<String, Map<String, List>>
进阶通过partitioningBy进行分区
特殊的分组,它分类依据是true和false,所以返回的结果最多可以分为两组
等同于
这个例子可能并不能看出分区和分类的区别,甚至觉得分区根本没有必要,换个明显一点的例子:
返回值的键仍然是布尔类型,但是它的分类是根据范围进行分类的,分区比较适合处理根据范围进行分类
来一个本人在工作中遇到的样例
总结
通过使用Stream API可以简化代码,同时提高了代码可读性,赶紧在项目里用起来。讲道理在没学Stream API之前,谁要是给我在应用里写很多Lambda,Stream API,飞起就想给他一脚。
我想,我现在可能爱上他了【嘻嘻】。同时使用的时候注意不要将声明式和命令式编程混合使用。
文章转载自公众号:码哥字节
