说到Stream便容易想到I/O Stream,而实际上,谁规定“流”就一定是“IO流”呢?在Java 8中,得益于Lambda所带来的函数式编程,引入了一个全新的Stream概念,用于解决已有集合类库既有的弊端。
引言
传统集合的多步遍历代码
几乎所有的集合(如 Collection 接口或 Map 接口等)都支持直接或间接的遍历操作。而当我们需要对集合中的元 素进行操作的时候,除了必需的添加、删除、获取外,最典型的就是集合遍历。例如:
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
public class DemoForEach {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
Collections.addAll(list, "Java", "C", "Python", "Hadoop", "Spark");
for (String s : list) {
System.out.println(s);
}
}
}
运行程序,控制台输出:
Java
C
Python
Hadoop
Spark
这是一段非常简单的集合遍历操作:对集合中的每一个字符串都进行打印输出操作。
循环遍历的弊端
Java 8的Lambda让我们可以更加专注于做什么(What),而不是怎么做(How)。
现在,我们仔细体会一下上例代码,可以发现:
- for循环的语法就是“怎么做”
- for循环的循环体才是“做什么”
为什么使用循环?因为要进行遍历。但循环是遍历的唯一方式吗?遍历是指每一个元素逐一进行处理,而并不是从第一个到最后一个顺次处理的循环。前者是目的,后者是方式。
试想一下,如果希望对集合中的元素进行筛选过滤:
- 将集合A根据条件一过滤为子集B;
- 然后再根据条件二过滤为子集C。
那怎么办?在Java 8之前的做法可能为:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class DemoNormalFilter {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
Collections.addAll(list, "Java", "C", "Python", "Hadoop", "Spark");
System.out.print("筛选前的集合:");
for (String s : list) {
System.out.print(s + ",");
}
System.out.println();
System.out.print("经过条件1筛选后的集合:");
for (String s : list) {
if (s.length() >= 4) {
System.out.print(s + ",");
}
}
System.out.println();
System.out.print("经过条件2筛选后的集合:");
for (String s : list) {
if (s.length() >= 5) {
System.out.print(s + ",");
}
}
System.out.println();
}
}
运行程序,控制台输出:
筛选前的集合:Java,C,Python,Hadoop,Spark,
经过条件1筛选后的集合:Java,Python,Hadoop,Spark,
经过条件2筛选后的集合:Python,Hadoop,Spark,
这段代码中含有三个循环,每一个作用不同:
1、首先从头到尾,遍历输出集合。
2、然后筛选字符串长度大于等于4的元素,并输出。
3、最后筛选字符串长度大于等于5的元素,并输出。
每当我们需要对集合中的元素进行操作的时候,总是需要进行循环、循环、再循环。这是理所当然的么?不是。循环是做事情的方式,而不是目的。另一方面,使用线性循环就意味着只能遍历一次。如果希望再次遍历,只能再使用另一个循环从头开始。
那么,Lambda的衍生物Stream能给我们带来怎样更加优雅的写法呢?下面我们来看一下Stream的更优写法。
Stream的更优写法
下面来看一下借助Java 8的Stream API,什么才叫优雅:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Demo01Stream {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
Collections.addAll(list, "Java", "C", "Python", "Hadoop", "Spark");
list.stream()
.filter((s) -> s.length() >= 4)
.filter((s) -> s.length() >= 5)
.forEach((s) -> System.out.println(s));
}
}
运行程序,控制台输出:
Python
Hadoop
Spark
筛选的结果与上面的例子一致。
直接阅读代码的字面意思即可完美展示无关逻辑方式的语义:获取流、过滤长度小于4的、过滤长度小于5的、逐一打印。代码 中并没有体现使用线性循环或是其他任何算法进行遍历,我们真正要做的事情内容被更好地体现在代码中。
上面程序用到的方法:
stream()方法
利用stream()方法,来获取流。该方法是java.util.Collection接口中的一个默认方法,方法源码如下:
// 返回以该集合为源的序列流。
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
filter()方法
该方法是java.util.stream包中的Stream接口里的一个抽象方法,方法源码如下:
// 返回由与给定 {predicate} 匹配的此流元素组成的流。
Stream<T> filter(Predicate<? super T> predicate);
该方法的返回值是一个流,传入的参数是一个函数式接口:java.util.function.Predicate,该接口可以对某种类型的数据进行判断,然后返回一个布尔值。
forEach()方法
该方法是java.util.stream包中Stream接口里的一个抽象方法,方法源码如下:
// 对该流的每个元素执行操作。
void forEach(Consumer<? super T> action);
该方法没有返回值,传入的参数是一个函数式接口:java.util.function.Consumer。它的作用是:消费一个数据, 其数据类型由泛型决定。
上面例子的代码可以进行改善:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Demo02Stream {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
Collections.addAll(list, "Java", "C", "Python", "Hadoop", "Spark");
list.stream()
.filter((s) -> s.length() >= 4)
.filter((s) -> s.length() >= 5)
.forEach(System.out::println);
}
}
其实就是修改了forEach()方法中传入的参数,
System.out::println
(s) -> System.out.println(s)
这两者是等价的。
流式思想概述
注意:请暂时忘记对传统IO流的固有印象!
整体来看,流式思想类似于工厂车间的“生产流水线”。
当需要对多个元素进行操作(特别是多步操作)的时候,考虑到性能及便利性,我们应该首先拼好一个“模型”步骤 方案,然后再按照方案去执行它。
这张图中展示了过滤、映射、跳过、计数等多步操作,这是一种集合元素的处理方案,而方案就是一种“函数模型”。图中的每一个方框都是一个“流”,调用指定的方法,可以从一个流模型转换为另一个流模型。而最右侧的数字 3是最终结果。
这里的 filter 、 map 、 skip 都是在对函数模型进行操作,集合元素并没有真正被处理。只有当终结方法 count 执行的时候,整个模型才会按照指定策略执行操作。而这得益于Lambda的延迟执行特性。
备注:“Stream流”其实是一个集合元素的函数模型,它并不是集合,也不是数据构,其本身并不存储任何元素(或其地址值)。
Stream(流)是一个来自数据源的元素队列
- 元素是特定类型的对象,形成一个队列。 Java中的Stream并不会存储元素,而是按需计算。
- 数据源:流的来源。 可以是集合,数组等。
和以前的Collection操作不同, Stream操作还有两个基础的特征:
- Pipelining:中间操作都会返回流对象本身。 这样多个操作可以串联成一个管道, 如同流式风格(fluent style)。 这样做可以对操作进行优化, 比如延迟执行(laziness)和短路( short-circuiting)。
- 内部迭代:以前对集合遍历都是通过Iterator或者增强for的方式,显式的在集合外部进行迭代,这叫做外部迭代。 Stream提供了内部迭代的方式,流可以直接调用遍历方法。
当使用一个流的时候,通常包括三个基本步骤:获取一个数据源(source)→ 数据转换→执行操作获取想要的结果,每次转换原有Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以 像链条一样排列,变成一个管道。