Java 8 Stream 简介

  java.util.stream是Java 8 新引入的API,它有别于Java中的I/O Stream,I/O流是Java程序与外部数据输入和输出,例如从文件、网络的读写数据流。而Java8中的Stream它主要是针对集合的操作。
  集合是Java中使用最多的API,除了往集合里添加、删除数据,很多业务逻辑都涉及类似于数据库的操作,比如对数据按照类别进行分组、筛选和转换。一般可能是用迭代器直接遍历整个集合,一个一个的处理每一个数据元素。但如果这个集合非常大要是要处理大量元素又该怎么办呢?为了提高性能,需要并行处理,并利用多核架构。但写并行代码比用迭代器还要复杂,而且调试起来也够受的。
  为了程序设计得更轻松一点,节约宝贵时间,Java 8就提供大量数据处理的Stream实现。

使用Stream的区别

用Java 7时的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
List<Dish> lowCaloricDishes = new ArrayList<>();
// 筛选卡路里小于400的菜单
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());
}

用Java 8 Stream来实现:

1
2
3
4
5
6
7
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());

  为了利用多核架构并行执行这段代码,只需要把stream()换成parallelStream():

  从软件工程师的角度来看,新的方法有几个显而易见的好处:

  • 代码是以声明性方式写的:说明想要完成什么(比如filter过滤、map提取)而不是说明如何实。
  • 可以把几个基础操作链接起来,来表达复杂的数据处理流水线,在filter后面接上sorted、 map和collect操作。

  总结一下, Java 8中的Stream API的特性:1、声明性——更简洁,2、更易读可复合——更灵活3、可并行——性能更好。

Stream与集合的差别

  Stream到底是什么呢?简短的定义就是“从支持数据处理操作的源生成的元素序列”

  • 元素序列——和集合一样,Stream可以访问特定元素类型的一组有序值。因为集合是数据结构,集合讲的是数据,主要操作是对数据添加删除查找,而Stream讲的是计算,例如filter、 sorted和map等。
  • ——流会使用一个提供数据的源,如集合、数组或输入/输出资源。 注意,从有序集合生成流时会保留原有的顺序。由列表生成的流,其元素顺序与列表一致。
  • 数据处理操作——流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中的常用操作,如filter、 map、 reduce、 find、 match、 sort等。流操作可以顺序执行,也可并行执行。
    此外,流操作有两个重要的特点。
  • 流水线——很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大的流水线。流水线的操作可以看作对数据源进行数据库式查询。
  • 内部迭代——与使用迭代器显式迭代的集合不同,流的迭代操作是在背后进行的。

什么时候进行计算

  集合是一个内存中的数据结构,它包含数据结构中目前所有的值——集合中的每个元素都得先算出来才能添加到集合中。
  Stream则是在概念上固定的数据结构(你不能添加或删除元素),其元素则是按需计算的。例如要构建一个质数流(2, 3, 5, 7, 11, …),尽管质数有无穷多个。这个思想就是用户仅仅从流中提取需要的值,而这些值——在用户看不见的地方——只会按需生成。这是一种生产者-消费者的关系。从另一个角度来说,流就像是一个延迟创建的集合:只有在消费者要求的时候才会计算值。
  与此相反,集合则是急切创建的,以质数为例,要是想创建一个包含所有质数的集合,那这个程序算起来就没完没了了,因为总有新的质数要算,然后把它加到集合里面。当然这个集合是永远也创建不完的,消费者这辈子都见不着了。

遍历方式

  迭代器类似,Stream只能被遍历一次。遍历完之后,我们就说这个流已经被消费掉了。你可以从原始数据源那里再获得一个新的流来重新遍历一遍。
  集合需要用户手动去迭代,不管是for-each还是Iterator,都是要显式指定迭代方法,如果有并行计算的话,基本上就要自己管理所有的并行问题。而Stream是内部迭代,可以自动选择一种适合你硬件的数据表示和并行实现。
List<String> names = menu.stream().map(Dish::getName).collect(toList());例如这段代码里,就没有关于迭代的代码存在,但其内部已经实现了迭代功能。
  Stream和集合遍历的区别就是,内部迭代和外部迭代。

Stream操作

中间操作

  诸如filter、map、sorted等中间操作会返回另一个流。这让多个操作可以连接起来形成一个查询。重要的是,除非流水线上触发一个终端操作,否则中间操作不会执行任何处理——它们很懒。这是因为中间操作一般都可以合并起来,在终端操作时一次性全部处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
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]
  从打印的结果可以发现,对于filter、map、limit的操作,并不是单个操作完全执行完,再执行下一个操作,而是交替执行的。filter和map是两个独立的操作,但它们合并到同一次遍历中了。

终端操作

  终端操作会从流的流水线生成结果。其结果是任何不是流的值,比如List、 Integer,甚至void。

参考资料

  • 《Java 8 in Action》