电子书蓝奏云|https://zhuzi51.lanzoui.com/iJvQioaqb9a
随书代码
https://zhuzi51.lanzoui.com/iuSZ9oaqbde
4 引入流
为了解释集合是怎么工作的,想象一下你准备列出系列菜,组成一张菜单,然后再遍历一遍,把每盘才的热量加起来。或者,你可能选出那些热量比较低的菜,组成一张健康的特殊菜单。尽管集合对于几乎任何一个Java应用都是不可或缺的,但集合操作远远算不上完美。
- 很多业务逻辑都涉及类似的数据库的操作,比如对几道菜按照类别进行分组(比如全素)。或者查找出最贵的菜。
- 如果处理大量元素又该怎么办?为了提高性能,你需要并行处理,并利用多核架构。但写并行流代码比用迭代器还要复杂,而且调试起来也十分没意思!
4.1 流是什么
流是Java API的新成员,它允许你以声明性的方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现)。就现在来说,你可以把它们看成遍历数据集的高级迭代器。此外,流还可以以透明地并行处理,无需写任何多线程代码。
import lombok.Getter;import lombok.Setter;import org.apache.commons.lang.builder.ToStringBuilder;/*** @author study* @version 1.0* @date 2021/4/20 11:19*/@Getter@Setterpublic class Dish {private final String name;private final boolean vegetarian;private final int calories;private final Type type;public Dish(String name, boolean vegetarian, int calories, Type type) {this.name = name;this.vegetarian = vegetarian;this.calories = calories;this.type = type;}@Overridepublic String toString() {return new ToStringBuilder(this).append("name", name).toString();}public enum Type {MEAT, FISH, OTHER}public static final List<Dish> menu = Arrays.asList(new Dish("pork", false, 800, Dish.Type.MEAT),new Dish("beef", false, 700, Dish.Type.MEAT),new Dish("chicken", false, 400, Dish.Type.MEAT),new Dish("french fries", true, 530, Dish.Type.OTHER),new Dish("rice", true, 350, Dish.Type.OTHER),new Dish("season fruit", true, 120, Dish.Type.OTHER),new Dish("pizza", true, 550, Dish.Type.OTHER),new Dish("prawns", false, 400, Dish.Type.FISH),new Dish("salmon", false, 450, Dish.Type.FISH));}
4.2 流简介
流到底是什么?简短的定义“从支持数据处理操作的源生成的元素序列”。
- 元素序列——就像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值。因为集合是数据结构,所以它的主要目的是以特定的时间/空间复杂度存储和访问元素。但流的目的在于表达计算,比如filter、sorted、和map。集合讲的是数据,而流讲的是计算。
- 源——流会使用一个提供数据的源,比如集合、数组或I/O资源。从有序集合生成流是会保留原有的顺序。由列表生成的流,其元素顺序与列表一致。
- 数据处理操作——流的数据处理功能类似于数据库的操作,以及函数式编程语言中的常用操作,比如filter、map、reduce、find、math、sort等,流操作可以顺序执行,也可以并行执行。
- 流水线——很多流操作本身返回一个流,这样多个操作就可以链接起来,构成一个更大的流水线。
- 内部迭代——与集合使用迭代器进行显示迭代不同,流的迭代操作是后台进行的。
final List<String> threeHighCaloricDishNames = menu.stream()// 首先选出高热量的菜肴.filter(dish -> dish.getCalories() > 300)//获取菜名.map(Dish::getName).limit(3).collect(Collectors.toList());//只选头三个System.out.println(threeHighCaloricDishNames);
本例先是对menu调用stream方法,由菜单得到一个流。数据源是菜肴列表(菜单),它给流提供了一个元素序列。接下来,对流应用一些列数据处理操作:filter、map、limit和collect。除了collect之外,所有这些操作都会返回另一个流,这样它们就可以接成一条流水线。于是就可以看作对源的一个查询。最后,collect操作开始处理流水线,并返回结果(它和别的操作不一样,因为它返回的不是流,这这里是一个list。)在调用collect之前,没有任何结果产生,实际上根本就没有从menu里选择元素。可以这么理解:链中的方法调用都在排队等待,直到调用collect。
4.3 流与集合
Java现在的集合概念和新的流概念都提供了接口,来配合代表元素型有序值的数据接口。所谓有序,就是说我们一般是按顺序取值,而不是随机取用。
粗略的说,集合与流之间的差异在于什么时候进行计算。集合是一个内存中的数据结构,它包含数据结构中目前所有的值——集合中的每个元素都得先算出来才能添加到集合中(可以往集合里加东西或者删东西,但是不管什么时候,集合中的每个元素都是放在内存里的,元素都得先算出来才能成为集合的一部分)
相比之下,流则在概念上是固定的数据结构(不能添加或删除元素),其元素是按需计算的。这对编程有很大的帮助。
4.3.2 外部迭代与内部迭代
使用Collection接口需要用户去做迭代(比如for-each),这种称为外部迭代。相反,Stream库使用内部迭代——它帮你把迭代做了,还把得到的流值存在了某个地方,你只要给出一个函数说要干什么就可以了。下面的代码说明这这两种却别。
集合:使用for-each循环外部迭代
final ArrayList<String> names = new ArrayList<>();//显示顺序迭代菜单列表for (Dish dish : menu) {//提取名称并将其添加到累加器names.add(dish.getName());}
请注意,for-each还隐藏了迭代中的一些复杂性,for-each结构是一个语法糖,它背后的东西用Itearator对象表达出来会更丑陋。
集合:用背后的迭代器做外部迭代 显示迭代
final ArrayList<String> names = new ArrayList<>();final Iterator<Dish> iterator = menu.iterator();while (iterator.hasNext()) {final Dish dish = iterator.next();names.add(dish.getName());}
流:内部迭代
// 流:内部迭代final List<String> streamNames = menu.stream().map(Dish::getName)// 开始执行操作流水线:没有迭代.collect(Collectors.toList());
外部迭代一个集合,显示地取出每个项目在加以处理。
内部迭代时,项目可以透明地并行处理,或者以更优化的顺序进行处理。要是使用Java过去的外部爹地啊方式,这些优化都是很困难的。这似乎有点鸡蛋里挑骨头,但这差不多就是Java8引入流的理由了——Streams库的内部迭代可以自动选择一种适合你的硬件的数据表示和并行实现。与此相反,一旦选择了for-each这样的外部迭代。那基本上就要自己管理所有的并行问题了。
5 使用流
5.1 筛选
5.1.1 用谓词筛选
stream接口支持filter方法。该操作会返回一个谓词(一个返回boolean的函数)作为参数,并返回一个包括所有谓词的元素的流。
