集合是Java中使用最多的API。要是没有集合,还能做什么呢?几乎每个java应用程序都会制造处理集合。集合对于很多的编程任务来说都是非常基本的:它们可以让你把数据分组并且加以处理。为了解释集合是怎么工作的,想象一下你准备一系列菜,组成一张菜单,然后再遍历一次,把每盘菜的热量加起来。你可能想选出那些热量比较低的菜,组成一张健康的特殊菜单。尽管集合对于几乎任何一个Java应用程序都是不可或缺的,但是集合操作缺远远算不上完美。
A.很多任务逻辑都涉及类似于数据库的操作,比如对几道菜按照类别进行分类(比如全素菜肴),或者查找出最贵的菜。你自己用迭代器重新实现过这些操作多少遍?大部分数据库都允许你声明式地指定这些操作。比如,以下sql查询语句就可以选出热量较低的菜肴名称:select name from dishes where calorie < 400。你看,你不需要实现如何根据菜肴的属性进行筛选(比如利用得迭代器和累加器),你只需要表达你想要什么。这个基本的思路意味着,你用不着担心怎么去显示地实现这些查询语句。
B.要是要处理大量元素有该怎么办呢?为了提供性能,你需要并行处理,并且利用多核架构。但是写并行代码比用迭代器还要复杂,而且调试起来也够难受的!
那Java语言的设计者能做些什么,来帮助节约宝贵的时间,让你这个程序员活的轻松一点儿呢?你可能已经猜到了,答案就是流。
1.流是什么
流是Java API的新成员,它允许你以声明性方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现)。就现在来说,你可以把它们看成遍历数据集的高级迭代器。此外,流还可以透明地并行处理,你无需写任何多线程代码了!我们简单看看使用流的好处吧。下面两端代码都是返回低热量的菜肴名称的,并且按照卡路里排序,一个使用Java7,写的,另一个是用Java8的流写的。比较一下。不用担心Java8代码怎么写的:
之前(Java7):
1 List<Dish> lowCaloricDishes = new ArrayList<>(); 2 List<Dish> menu = Arrays.asList(new Dish("1", 100), new Dish("1", 300), new Dish("1", 500), 3 new Dish("1", 1000)); 4 for (Dish d : menu) { //使用迭代器筛选元素 5 if(d.getCalories() < 400){ 6 lowCaloricDishes.add(d); 7 } 8 } 9 Collections.sort(lowCaloricDishes, new Comparator<Dish>() { // 使用匿名类菜肴排序 10 11 @Override 12 public int compare(Dish o1, Dish o2) { 13 return Integer.compare(o1.getCalories(), o2.getCalories()); 14 } 15 }); 16 List<String> lowCaloricDishesName = new ArrayList<>(); 17 for(Dish d: lowCaloricDishes){ 18 lowCaloricDishesName.add(d.getName()); //处理排序后的菜名列表 19 }
在这段代码中,你用了一个“垃圾变量”lowCaloricDishes。它唯一的作用就是作为一次性中间容器。在Java8中,实现的细节被放在它本该归属的库里了。
1 List<String> lowCaloricDishesName = new ArrayList<>(); 2 lowCaloricDishesName = 3 menu.stream() 4 .filter(d -> d.getCalories() < 400) //选出400卡路里以下的菜肴 5 .sorted(Comparator.comparing(Dish::getCalories)) //按照卡路里排序 6 .map(Dish::getName) //提取菜肴的名称 7 .collect(Collectors.toList()); //将所有的名称保存在List中
为了利用多核架构并行执行这段代码,你只需要把stream()换成parallelStream();
1 lowCaloricDishesName = 2 menu.parallelStream() 3 .filter(d -> d.getCalories() < 400) //选出400卡路里以下的菜肴 4 .sorted(Comparator.comparing(Dish::getCalories)) //按照卡路里排序 5 .map(Dish::getName) //提取菜肴的名称 6 .collect(Collectors.toList()); //将所有的名称保存在List中
你可能会想,在调用parallelStream方法的时候到底发生了什么。用了多少个线程?对性能有多大的提升?这些问题在以后会详细的讨论。现在,你看出来了,从软件工程师的角度来看,新的方法有几个显而易见的好处。
A.代码是以声明性方法来写的:说明了想要完成什么(筛选热量低的菜肴)而不是说明了如何实现一个操作(利用循环和if条件等控制流语句)。你在之前也看到了,这种方法加上行为参数化让你可以轻松应对变化的需求:你很容易再创建一个代码版本,利用Lamba表达式来筛选高卡路里的菜肴,而不是去复制粘贴代码。
B .你可以把几个基础操作筛选链接起来,来表达复杂的数据处理流水线(在filter后面接上sorted、map和collect操作, 如下图所示),同时保持代码清晰可读。filter的结果被传给了sorted方法,再传给map方法,最后传给collect方法。
因为filter、sorted、map和collect等操作是与具体线程模型无关的高层次构件,所以它们的内部实现可以是单线程的,也可能透明地充分利用你的多级架构!在实践中,这意味着你用不着为了让某些数据处理任务并行而去操心线程和锁了,Stream API都替你想好了!
2.流简介
要讨论流,我们先来谈谈集合,这是最容易上手的方法。Java8中的集合支持一个新的stream方法,它会返回一个流(接口定义在java.util.stream.Stream里)。你在后面会看到,还有很多的方式来可以得到流,比如利用数值范围或从I/O资源生成的流元素。
那么,流到底是什么呢?简短的定义就是"从支持数据处理操作的源生成的元素序列"。让我们一步步剖析这个定义。
A.元素序列--就像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值。因为集合是数据结构,所以它的主要目的是以特定的时间/空间复杂度存储和访
问元素(ArrayList和LinkedList)。但是流的目的在于表达计算,比如你前面简单的filter、sorted和map。集合讲的是数据,流讲得是计算。我们在后面几节详细解释这个思想。
B.源--流会使用一个提供数据的源,如集合、数组或者输入/输出资源。请注意,从有序集合生成流时会保留原有的顺序。由列表生成的流,其元素顺序与列表一致。
C.数据处理操作--流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中的常用操作,如filter、map、reduce、find、match、sort等等。流操作可以顺序执行,也可以并行执行。
此外,流操作有两个重要的特点。
A.流水线--很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大的很大的流水线。这让我们以后的一些优化成为了可能,如延迟和短路。流水线的操作可以可以看做对数据源进行数据库式查询。
B.内部迭代--与使用迭代器显示迭代的集合不同,流的迭代操作是在背后进行的。
让我们来看一段能够体现所有这些概念的代码:
Dish类的定义:
1 package com.example.demo13; 2 3 public class Dish { 4 private final String name; 5 private final boolean vegetarion; 6 private final int calories; 7 private final Type type; 8 public enum Type{ 9 MEAT, FISH, OTHER 10 } 11 public Dish(String name, boolean vegetarion, int calories, Type type) { 12 this.name = name; 13 this.vegetarion = vegetarion; 14 this.calories = calories; 15 this.type = type; 16 } 17 public String getName() { 18 return name; 19 } 20 public boolean isVegetarion() { 21 return vegetarion; 22 } 23 public int getCalories() { 24 return calories; 25 } 26 public Type getType() { 27 return type; 28 } 29 30 }
main方法所在类的定义:
1 package com.example.demo13; 2 3 import java.util.Arrays; 4 import java.util.List; 5 import java.util.stream.Collectors; 6 7 public class Demo13 { 8 public static void main(String[] args) { 9 List<Dish> menu = Arrays.asList( 10 new Dish("pork", false, 800, Dish.Type.MEAT), 11 new Dish("beef", false, 700, Dish.Type.MEAT), 12 new Dish("chicken", false, 400, Dish.Type.MEAT), 13 new Dish("french fries", true, 530, Dish.Type.OTHER), 14 new Dish("rice", true, 350, Dish.Type.OTHER), 15 new Dish("season fruit", true, 120, Dish.Type.OTHER), 16 new Dish("pizza", true, 550, Dish.Type.OTHER), 17 new Dish("prawns", false, 300, Dish.Type.FISH), 18 new Dish("salmon", false, 450, Dish.Type.FISH)); 19 List<String> treenHighCaloricDishNames = 20 menu.stream(). //从menu获得流(菜肴列表) 21 filter(d -> d.getCalories() > 300). //建立流水线的操作:首先选出高热量的菜肴 22 map(Dish::getName). //获取菜名 23 limit(3). //只选择前面三个 24 collect(Collectors.toList()); //结果保存在另一个List集合中 25 System.out.println(treenHighCaloricDishNames); 26 } 27 }