最近打算写一个Stream流的系列,这是基于我看完《java8 in action》英文原版 Stream部分的一些总结,如果你看《java8 in action》有点难以理解 ,那么你可以参考一下我的博客,可以让你清晰Stream的使用和原理。整个流的系列大概按以下方向写:
ps:博文中的实例代码,用到 FunctionalInterface 接口的地方都用了lambda或者方法引用,所以,如果你对lambda或者方法引用不熟悉或不了解,先看一下下面两篇博文:
Java8新特性1:lambda表达式入门--由浅入深,从单发步枪迈向自动步枪
Java8新特性2:方法引用--深入理解双冒号::的使用
1、引入流
我们先看看Stream 流的由来:《java8 in action》的作者对现有的在开发中的集合使用,做了一些思考,并引出了流这个东西(上图)。大概意思是:
目前我们在几乎所有开发中都会用到集合,但是目前集合在程序开发中的表现还不够完美,比如你利用集合处理大量数据时,你不得不面对性能问题,不得不考虑进行并行代码的编写,这些工作都是比较繁重的,于是作者便创造了Stream 流。相比较Collection集合来说,Stream在开发中就具有许多独特的优点,这些优点你可以先不用理解,知道就行,我们会在下面的案例代码中直观感受到:
2、举个例子,如何用Stream流的编码方式实现需求
我们先不管Stream的概念,先看看作者举的一个例子:有一些菜单menu,现在需要你写一个菜单筛选程序,选出菜单中热量低于400的菜品的菜名。对于这个简单需求,我们分两个方法实现,一个使用jdk8以前的Collection API实现,一个使用jdk8的Stream流实现。我们先简单看看二者的实现代码的区别:
菜品的bean:Dish.java
package com.aigov.java8_newfeatures.stream;
import lombok.Data;
/**
* @author : aigoV
* @date :2019/10/21
* 菜单bean
**/
@Data
public class Dish {
private final String name;//菜名
private final boolean vegetarian;//是否是素食
private final int calories;//热量,卡路里
private final Type type;//类型
public enum Type { MEAT, FISH, OTHER }
public Dish(String name, boolean vegetarian, int calories, Type type) {
this.name = name;
this.vegetarian = vegetarian;
this.calories = calories;
this.type = type;
}
@Override
public String toString() {
return "Dish{" +
"name='" + name + '\'' +
'}';
}
}
需求实现类:simpleStream.java
package com.aigov.java8_newfeatures.stream;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author : aigoV
* @date :2019/10/21
* Stream流
**/
public class SimpleStream {
/**
* 使用传统的集合方式
**/
public static List getDishNamesByCollections(List menu) {
List lowCalories = new ArrayList<>();
//filter 400 过滤
for (Dish d : menu) {
if (d.getCalories() < 400) {
lowCalories.add(d);
}
}
//sort 排序
Collections.sort(lowCalories, Comparator.comparingInt(Dish::getCalories));
//nameList for Calories<400 将菜品名放入一个集合
List dishNameList = new ArrayList<>();
for (Dish d : lowCalories) {
dishNameList.add(d.getName());
}
return dishNameList;
}
/**
* 用时java8 Stream流的方式处理数据集
* @param menu
* @return
*/
public static List getDishNamesByStream(List menu){
List getDishNamesByStream = menu.parallelStream()//转为可利用多核架构并行执行的Stream
.filter(d -> d.getCalories()<400)//过滤卡路里小于400的菜
.sorted(Comparator.comparing(Dish::getCalories))//将过滤后的Stream按卡路里 小->大 排序
.map(Dish::getName)//提取过滤后的菜品流中的菜名
.collect(Collectors.toList());//最后将流转为集合
return getDishNamesByStream;
}
public static void main(String[] args) {
/** 模拟菜单**/
List 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, 300, Dish.Type.FISH),
new Dish("salmon", false, 450, Dish.Type.FISH));
List dishNamesByCollections = getDishNamesByCollections(menu);
System.out.println("集合处理方式:"+dishNamesByCollections);
List getDishNamesByStream = getDishNamesByStream(menu);
System.out.println("Stream处理方式:"+getDishNamesByStream);
}
}
打印结果:
上面的需求实现类中,我们可以看到Collection和Stream两个实现方式相比较下,Stream在直观上最大的优点就是:代码更加简洁;而且Stream允许你声明式的编码,也就是从代码上直观展示出我们干嘛干嘛(我要筛选热量<400的菜等等),而不再冗杂的循环和if条件;此外,上面代码,我直接用到了parallelStream()方法,这个方法可以利用多核架构并行执行代码,无论是并行处理的性能还是线程条数,都不需要你在考虑。
1、什么是流?
上面我们只是引出了Stream这个东西,并简单对比了集合与流。那么Stream 流 到底怎么理解呢?
在《java in action》书中的定义是:"a sequence of elements from a source that supports data processing operations.",以我小学一级英语翻译过来就是“支持数据处理操作的一个source(资源?) 中的元素序列”。这个定义似乎有点不太通俗,下面让我们分解一下这个定义,以通俗的的理解什么Stream,Stream的结构和组成。
Sequence of elements(元素序列):这个其实就是 source 里面的东西。对应上面的菜单程序,它其实就Dish部分,即源码中的黄色框框部分:
source(数据源) :Stream流的作用就是操作数据,那么source 就是为Stream提供可操作的源数据的。对应上面的菜单程序,它就是menu(下图。一般,集合、数组或I/OI/O resources 都可以成为Stream的source :
Data processing operations(数据处理操作):上面菜单程序代码中出现的filter、sorted、map、collect,以及我们后来会用到的reduce、find、match等都属于Stream 的一些操作数据的方法接口。这些操作可以顺序进行,也可以并行执行。
Pipelining(管道、流水线):Stream对数据的操作类似数据库查询,也像电子厂的生产流线一样,Stream的每一个中间操作(后面解释什么是中间操作)比如上面的filter、sorted、map,每一步都会返回一个新的流,这些操作全部连起来就是想是一个工厂得生产流水线, like this:
Internal iteration(内部迭代):Stream API 实现了对数据迭代的封装,不用你再像操作集合一样,手动写for循环显示迭代数据。
2、Stream 流的使用。
java8 的 Stream API 提供了很多数据操作接口,这里面一些常用接口其实我们在第一部分那个例子中就看到了,有基础的朋友其实看了第一部分那个例子已经可以进行一些简单的Stream操作。那么现在我们详细来看看Stream的一些基本操作和操作概念,让我们不只停留在会用。
我们想看看我们最开始使用到的菜单程序源码,我做了一些标注:
上面我们引出了两个新的概念,中间操作,终端操作(其实我们上面也提到了),然后你可以看到两类操作:
3、什么叫中间操作,中间操作的执行顺序?
可以连接起来的Stream流操作,诸如filter或sorted等可以返回一个Stream 流的操作,就叫中间操作。只有存在一个终端操作,且触发了终端操作,中间操作才会开始执行。
现在我们为菜单程序编写一个新的方法,以便直观看到中间操作的执行循序:
//查找菜单中卡路里大于300的菜名 ,只需要输出四个就行
public static List getDishNamesByStream(List menu){
List getDishNamesByStream =
menu.stream()
.filter(d -> {
System.out.println("执行filter—>" + d.getName());
return d.getCalories() > 300;
})
.map(d -> {
System.out.println("执行mapping—>" + d.getName());
return d.getName();
})
.limit(4)
.collect(toList());
return getDishNamesByStream;
}
执行结果:
我们通过控制台的打印信息可以知道,虽然filter和map是两个独立的中间操作,但它们合并到同一次遍历中执行,这就叫作循环合并。
4、什么叫终端操作?
可以启动中间操作和关闭中间流的操作称为终端操作,终端操作会从流的流水线(中间操作)生成结果。其结果是任何非Stream的值,比如可能是List、Integer,甚至void。例:在下面的流水线中,forEach作为一个流的终端操作,它只是打印一数据,并没有返回数据:
menu.stream().forEach(System.out::println);
5、 Stream 流的生命周期
同一个流只能遍历一次,遍历完后,这个流就已经被消费掉了。你如果还需要在遍历,可以从原始数据源那里再获得一个新的流来重新遍历一遍。
举个例子:
同一个流 s 被两次用于forEach的终端操作,此时控制台不报错:
但是你从原始数据源menu那里再获得一个新的流在操作就可以:
6、Stream的内部迭代与Collection的外部迭代
使用Collection需要你自己去做迭代(比如用for-each),这称为外部迭代。 但是Streams API提供了内部迭代,也就是说它帮你把迭代操作实现并封装了,还把得到的流存在了某个地方。举个例子:
集合:用for-each循环外部迭代:
List names = new ArrayList<>();
for(Dish d: menu){
names.add(d.getName());
}
集合:用背后的迭代器做外部迭代
List names = new ArrayList<>();
Iterator iterator = menu.iterator();
while(iterator.hasNext()) {
Dish d = iterator.next();
names.add(d.getName());
}
流:内部迭代
List names = menu.stream()
.map(Dish::getName)
.collect(toList());
是的,你没看到迭代代码
总的来说,Stream API 提供的内部迭代可以自动进行并行处理,或者用更优化的顺序进行处理。相比Java过去集合用的那种外部迭代方法,一旦你写for-each,选择了外部迭代,那你基本上就要自己处理所有的并行问题了,这些优化都是很繁杂的。
1、流的使用一般包括三件事(参照本文开头的案例代码):
2、Stream API提供的一些常用操作接口:
Java8新特性1:lambda表达式入门--由浅入深,从单发步枪迈向自动步枪
Java8新特性2:方法引用--深入理解双冒号::的使用
Java8新特性3:Stream1——什么是Stream,Stream的特性,如何使用Stream,Stream与Collection集合的区别
Java8新特性3:Stream2—一文详解Stream API,让你快速理解Stream Api提供的诸多常用方法
Java8新特性3:Stream3—数值流与对象流的转化及其方法使用