4.函数式数据处理-引入流

文章是个人阅读《Java8实战》过程中的重点摘抄,可能晦涩,没有示例代码,后续会补充总结完善。

本章内容

  • 什么是流
  • 集合与流
  • 中部迭代和外部迭代
  • 中间操作与终端操作

核心问题

1.流的定义是什么?流有哪些优点?流的特点是什么?
2.流与集合的两个对比点是什么?
3.流的两个操作步骤是什么?分别有哪些基本操作。

概述

集合是java中使用最多的API。集合可以让你把数据分组,然后对数据加以处理。

但是集合的操作并算不上完美,当我们需要处理大量元素的时候,为了提供性能,需要使用并行处理,但是并行处理比迭代器还要复杂,且不易调试。另外,我们对接好的操作为什么不能像SQL查询一样简单易用呢(我们做筛选必须使用迭代器)。


4.1 流是什么

流是javaAPI的成员,他允许我们以声明性方式处理数据集合。

我们以一个示例来展示使用流和不适用的对比:返回低热量的菜肴名称,并按照卡路里排序。
java7

// 用累加器筛选卡路里小于400的元素
List<Dish> lowCaloricDishes = new ArrayList<>();
for(Dish d: menu){
	if(d.getCalories() < 400){
		lowCaloricDishes.add(d);
	}
}
// 用匿名类对元素进行排序
Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
	public int compare(Dish d1, Dish d2){
		return Integer.compare(d1.getCalories(), d2.getCalories());
	}
});
// 获取排序后的菜名列表
List<String> lowCaloricDishesName = new ArrayList<>();
for(Dish d: lowCaloricDishes){
	lowCaloricDishesName.add(d.getName());
}

java8

import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toList;
// 非并行
List<String> lowCaloricDishesName = menu.stream().filter(d -> d.getCalories() < 400).sorted(comparing(Dish::getCalories)).map(Dish::getName).collect(toList());
// 并行
List<String> lowCaloricDishesName = menu.parallelStream().filter(d -> d.getCalories() < 400).sorted(comparing(Dishes::getCalories)).map(Dish::getName).collect(toList());

通过对比两种写法,java8API的优点显而易见。代码是以声明性(想要完成什么而不是说明如何实现)方式写的,通过链式操作表达复杂的数据处理流水线。

  • 声明性,更简洁易读
  • 可复用,更灵活
  • 可并行,更好的性能

因为filter、sorted、map、collect等操作是与具体线程模型无关的高层组件,所以他们的内部实现可以是单线程,也可能利用了多核架构。因此用户不用为了让数据处理任务并行而操心线程和锁,SteamAPI都已经实现了。
在这里插入图片描述


4.2 流简介

流的定义:从支持数据处理操作的源生成的元素序列。下面我们来剖析这个定义。

  • 元素序列:像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值。集合是数据结构,其目的是为了 以特定的时间/空间复杂度 存储和访问元素。流的目的在于表达计算。
  • 源:流会使用一个提供数据的源,如集合、数组、输入/输出资源。注意,从有序列集合生成的流会保留顺序,由列表生成的流的顺序与原来列表一致。
  • 数据处理操作:流的数据操作支持函数式编程中的常用操作,如filter、map、find、match、sort等。流可以顺序执行,也可以并行执行。

流的两个特点

  • 流水线:很多流的操作会返回流,这样多个操作就可以链接起来,形成流水线。流水线操作可以看作为对数据源进行数据库式查询。
  • 内部迭代:与使用迭代器显示迭代的集合不同,流的迭代操作是在背后进行的。

来看一段能够体现这些概念的示例:从菜单中查找热量最高的三道菜的菜名

import static java.util.stream.Collectors.toList;
List<String> threeHighCaloricDishNames =
	// 建立操作流水线
	menu.stream()
		// 选出热量高于300菜肴
		.filter(d -> d.getCalories() > 300)
		// 获取菜名
		.map(Dish::getName)
		// 选取前三个
		.limit(3)
		// 将结果保存在另一个list中
		.collect(toList());
System.out.println(threeHighCaloricDishNames);

剖析:我们使用了声明性的方式来处理菜单数据,即描述了对菜单需要做什么(查找热量最高的三道菜的菜名),而没有去实现筛选、提取、截断功能,StreamAPI已经自带了。

  1. 我们先是对menu调用stream方法,由menu得到一个流。数据源是菜肴列表(menu),他为流提供了一个元素序列
  2. 接下来,对流应用一系列数据处理操作:filter、map、limit和collect。除了collect之外,所有这些操作都会返回另一个流,这样它们就可以接成一条流水线,于是就可以看作对源的一个查询。
  3. 最后,collect操作开始处理流水线,并返回结果(它和别的操作不一样,因为它返回的不是流,在这里是一个List)。

在调用collect之前,没有任何结果产生,实际上根本就没有从menu里选择元素。你可以这么理解:链中的方法调用都在排队等待,直到调用collect。下图显示了流操作的顺序: filter、 map、 limit、 collect,每个操作简介如下。
在这里插入图片描述


4.3 流与集合

java现有的集合和新的流都提供了接口,来配合代表元素型有序值的接口。

粗略的讲,集合与流之间的差别就在于什么时候进行计算。集合是内存中的数据结构,他包含数据结构中目前所有的值,集合中的元素必须先计算出来才能添加到集合中。相比之下,流则是在概念上固定的数据结构(不可删除和添加元素),其元素是按需计算的(这个思想就是用户仅仅从流中提取需要的值,而这些值可能是用户看不见的地方),这是一种生产者-消费者的关系。从另一个角度来说,流就像是一个延迟创建的集合,只有在消费者要求的时候才计算。【有点难以理解!】

以᠎质数为例,要是想创建一个包含所有᠎质数的集合,那这个程序算起来就没完没了了,因为总有新的᠎质数要算,然后把它加到集合里面。当然这个集合是永远也创建不完的,消费者永远都见不到。

4.3.1 只能遍历一次

流只遍历一次,遍历完了之后,这个流就已经被消费了。我们无法继续遍历被消费的流,会抛出java.lang.IllegalStateException异常。我们就要从源获取一个新的流来遍历。这和集合的迭代器类似。

4.3.2 外部迭代和内部迭代

使用Colletion接口需要用户做迭代(比如for-each),这称作外部迭代。Stream库使用内部迭代,在库的内部会做迭代,并把得到的流的值存在某个地方。

// for-each迭代
// for-each背后是使用Iterator迭代。
List<String> names = new ArrayList<>();
for(Dish d: menu){
	names.add(d.getName());
}
// Iterator迭代
List<String> names = new ArrayList<>();
Iterator<String> iterator = menu.iterator();
while(iterator.hasNext()) {
	Dish d = iterator.next();
	names.add(d.getName());
}
// Stream迭代
List<String> names = menu.stream().map(Dish::getName).collect(toList());

Stream库的内部迭代可以自动选择一种适合你硬件的数据表示和并行实现,在类库的内部管理并行。


4.4 流操作

Stream接口定义了许多操作,他们可以分为两类,中间操作和终端操作。

4.4.1 中间操作

简单讲,中间操作就是StreamAPI中返回一个流的操作。例如filter、sorted等,这样可以多个操作链接起来形成一个流水线查询。在流水线上除非出发一个终端操作,否则不会执行任何处理。这是因为中间操作一般都可以合并起来,在终端操作时一次性全部处理。

为了探究中间操作具体做了什么,我们看下面这样一个示例,我们在中间过程中打印一些信息。

List<String> names =
	menu.stream()
		.filter(d -> {
			System.out.println("filtering" + d.getName());
			return d.getCalories() > 300;
		})
		.map(d -> {
			System.out.println("mapping" + d.getName());
			return d.getName();
		})
		.limit(3)
		.collect(toList());
System.out.println(names);
// 打印结果
filtering pork
mapping pork
filtering beef
mapping beef
filtering chicken
mapping chicken
[pork, beef, chicken]

从此例中可以看到,有好几种优化利用了流的延迟性质。第一,尽管很多菜的热量都高于300卡路里,但只选出了前三个!这是因为limit操作和一种称为短路的技巧。第二,尽管filter和map是两个独立的操作,但它们合并到同一次遍历中了(我们把这种技术叫作循环合并

4.4.2 终端操作

终端操作会从流的流水线上生成结果,其结果是任何不是流的值,例如List、void、Interger。

4.4.3 使用流

流的使用一般包括三件事:

  • 一个数据源(如集合),来执行一个查询。
  • 一个中间操作链,形成一条流的流水线。
  • 一个终端操作,执行流水线,并能生成结果。

流的流水线思想类似于构建器模式,在构建器模式中有一个调用链用来设置一套配置,接着调用build方法生成结果。


4.5 小结

  • 流是 从支持数据处理操作的源生成的一系列元素。
  • 流利用内部迭代器:迭代通过StreamAPI内部实现了。
  • 流操作有两类:中间操作和终端操作。中间操作会返回一个流,并可以链接起来形成一个流,但不会产生任何结果。终端操作会返回一个非流的值,并处理流水线以返回结果。
  • 流中的元素是按需计算的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值