Java开发笔记(七十二)Java8新增的流式处理

时间:2021-01-08 15:43:18

通过前面几篇文章的学习,大家应能掌握几种容器类型的常见用法,对于简单的增删改和遍历操作,各容器实例都提供了相应的处理方法,对于实际开发中频繁使用的清单List,还能利用Arrays工具的asList方法给清单对象做初始化赋值,另外提供了专门的Collections工具进行排序、求最大元素、求最小元素等操作。那么涉及到更加复杂的数据处理,游荡如何有针对性地筛选和进一步加功能?
依次遍历目标容器,对所有元素逐个加以分析判断,并酌情将具体数据调整至满意的状态,这种千篇一律的业务流程固然能够解决问题,可惜由此带来的副作用是显而易见的,包括但不限于:代码冗长、分支众多、逻辑繁琐、不易重用等等。为了改进相关业务逻辑的编程方式,帮助开发者形成良好的编码风格,Java的每次版本更新都试图给出有效的解决方案,其中影响深远的当数Java8推出的两项新特性:新增的泛型接口与流式处理。关于前一个泛型接口特性,用于容器操作的泛型接口主要有三个,分别是断言接口、消费接口和函数接口,有关的应用案例可参见之前的泛型接口文章,这里不再赘述。真正具有革命性意义的才是本文的主角——流式处理。
所谓流,隐含着流水线的意思,也就是由开发者事先设定一批处理指令,说明清楚每条指令的前因后果,然后启动流水线作业,即可得到最终的处理结果。流式处理的精髓在于一气呵成,只要万事俱备,决不拖泥带水。开展流式处理主要包括三个步骤:获得容器的流对象、设置流的各项筛选和加工指令,以及规划处理结果的展示形式。下面就分别予以详细介绍。

1、获得容器的流对象
Java8给每种容器都准备了两条流水线,一条是串行流,另一条是并行流。串行流顾名思义各项任务是前后串在一起的,只有处理完前一项任务,才能继续执行后一项任务。调用容器实例的stream方法即可获得该容器的串行流对象,而调用容器实例的parallelStream方法可获得该容器的并行流对象。
流对象的获取操作同时也是流式处理的开始指令,每次进行流式处理之前,都必须先获取当前容器的流对象,要么获取串行流,要么获取并行流。

2、设置流的各项筛选和加工指令
不管是串行流还是并行流,它们承载的都是容器内部的原始数据,这些原材料要经过各道加工工序,之后才会得到具备初步形态的半成品。加工数据期间所调用的流方法说明如下:
filter:按照指定条件过滤。即筛选出符合条件的那部分数据。
sorted:根据指定字段对所有记录排序。可选择升序或者降序。
map:映射成指定的数据类型。
limit:只取前面若干条数据。
distinct:去掉重复记录。保证每条记录都是唯一的。
以上的加工方法属于流式处理的中间指令,每次流水线作业都允许设置一条或者多条中间指令。

3、规划处理结果的展示形式
前一步的各项加工处理完毕,还要弄个包装才能输出最终的成品,也就是这条流水线生产出来的数据到底长什么模样。结果数据的记录包装有三种形式,分别对应如下的三个方法:
count:统计结果数据的数量。
forEach:依次遍历结果数据,并逐条进行个性化处理。
collect:搜集和整理结果数据,并返回指定格式的清单记录。
上面的三个包装方法属于流式处理的结束指令,每次流水线作业必须配备有且仅有其中的一条结束指令。

接下来列举几个实际应用的业务场景,看看采取流式处理时该如何编码。首先准备一个原始的苹果清单,后续将对这个苹果清单发动流水作业。原始清单的获取代码示例如下:

	// 获取默认的苹果清单
private static ArrayList<Apple> getAppleList() {
ArrayList<Apple> appleList = new ArrayList<Apple>();
appleList.add(new Apple("红苹果", "RED", 150d, 10d));
appleList.add(new Apple("大苹果", "green", 250d, 10d));
appleList.add(new Apple("红苹果", "red", 300d, 10d));
appleList.add(new Apple("大苹果", "yellow", 200d, 10d));
appleList.add(new Apple("红苹果", "green", 100d, 10d));
appleList.add(new Apple("大苹果", "Red", 250d, 10d));
return appleList;
}

然后需要统计红苹果总数的话,可通过下列的流式代码开展统计操作:

		// 统计红苹果的总数
long redCount = getAppleList().stream() // 串行处理
.filter(Apple::isRedApple) // 过滤条件。专门挑选红苹果
.count(); // 统计记录个数
System.out.println("红苹果总数=" + redCount);

注意到上述代码的filter方法内部出现了方法引用,的确流式处理的主要方法都预留了函数式接口的调用,所以经常会在流式代码中看到五花八门的方法引用与Lambda表达式。比如下面的结果遍历代码就在forEach方法中填充了Lambda表达式:

		// 对每个红苹果依次进行处理
getAppleList().stream() // 串行处理
.filter(Apple::isRedApple) // 过滤条件。专门挑选红苹果
.forEach(s -> System.out.println("当前颜色为"+s.getColor())); // 逐条开展操作

当然流水作业更常见的输出另一串清单数据,此时流式处理的结束指令就得采用collect方法。下面便是从原始清单中挑出红苹果清单的流式代码:

		// 挑出红苹果清单
List<Apple> redAppleList = getAppleList().stream() // 串行处理
//.parallelStream() // 并行处理
.filter(Apple::isRedApple) // 过滤条件。专门挑选红苹果
.sorted(Comparator.comparing(Apple::getWeight)) // 按苹果重量升序排列
//.sorted(Comparator.comparing(Apple::getWeight).reversed()) // 按苹果重量降序排列
.limit(3) // 只取前几条数据
.distinct() // 去掉重复记录
.collect(Collectors.toList()); // 返回一串清单
System.out.println("红苹果清单=" + redAppleList.toString());

结果清单可能不需要完整的苹果信息,只需列出苹果名称字段,那么得调用map方法把完整的苹果信息映射为单个的名称字段。此时的筛选代码变成下面这样:

		// 挑出去重后的苹果名称清单
List<String> allNameList = getAppleList().stream() // 串行处理
.map(Apple::getName) // 映射成新的数据类型
.distinct() // 去掉重复记录
.collect(Collectors.toList()); // 返回一串清单
System.out.println("苹果名称去重后的清单=" + allNameList.toString());

除了普通的清单,collect方法还能返回分组清单,也就是把结果数据按照某种条件进行分组,再统计每个分组的成员数目。仍以苹果清单为例,红苹果可通过名称或者产地分组,分组的同时计算每个小组里各有多少粒苹果。于是形成了以下的分组计数代码:

		// 按照名称统计红苹果的分组个数
Map<String, Long> redStatisticCount = getAppleList().stream() // 串行处理
.filter(Apple::isRedApple) // 过滤条件。专门挑选红苹果
.collect(Collectors.groupingBy(Apple::getName, Collectors.counting())); // 返回分组计数
System.out.println("红苹果分组计数=" + redStatisticCount.toString());

分组计数仅仅是简单统计各组的成员数量,有时还想单独计算某个字段的统计值,比如每个小组里的苹果总价各是多少?这时collect方法必须同时完成两项任务,第一项要根据某种条件分组,第二项要对各组的苹果价格求和,如此改造之后的分组求和代码如下所示:

		// 按照名称统计红苹果的分组总价
Map<String, Double> redPriceSum = getAppleList().stream() // 串行处理
.filter(Apple::isRedApple) // 过滤条件。专门挑选红苹果
.collect(Collectors.groupingBy(Apple::getName, Collectors.summingDouble(Apple::getPrice))); // 返回分组并对某字段求和
System.out.println("红苹果分组总价=" + redPriceSum.toString());

观察以上的具体案例,发现流式处理的代码相当连贯,每个步骤该做什么事情都一清二楚,中间没有许多繁复的流程控制,唯有一条条分工明确的处理指令,同时充分发挥了方法引用及Lambda表达式的便利性,使得原本令人头痛的容器加工变成了有章可循的流水线作业,从而极大地提高了开发者的编码效率。

更多Java技术文章参见《Java开发笔记(序)章节目录