流是什么
流是Java API的新成员,它允许你以声明性的方式处理数据集合。可以看成遍历数据集的高级迭代。流可以透明地并行处理,无需编写多线程代码。我们先简单看一下使用流的好处。下面两段代码都是用来返回年龄小于14岁的初中生的姓名,并按照年龄排序。
- Java 8之前的方式:
List<Student> students = Arrays.asList(
new Student("张初一", 13, false, Student.Grade.JUNIOR_ONE),
new Student("李初二", 14, false, Student.Grade.JUNIOR_TWO),
new Student("孙初三", 15, false, Student.Grade.JUNIOR_THREE),
new Student("王初一", 12, false, Student.Grade.JUNIOR_ONE),
new Student("钱初二", 14, false, Student.Grade.JUNIOR_TWO),
new Student("周初三", 16, false, Student.Grade.JUNIOR_THREE));
List<Student> resultStudent = new ArrayList<>(); //垃圾变量,一次性的中间变量
//foreach循环,根据条件筛选元素
for (Student student : students) {
if (student.getAge() < 14) {
resultStudent.add(student);
}
}
//匿名类,给元素排序
Collections.sort(resultStudent, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return Integer.compare(o1.getAge(), o2.getAge());
}
});
List<String> resultName = new ArrayList<>();
//foreach循环,获取元素属性
for (Student student : resultStudent) {
resultName.add(student.getName());
}
- Java 8流的方式:
List<String> resultName = students.stream()
.filter(student -> student.getAge() < 14) //年龄筛选
.sorted(Comparator.comparing(Student::getAge)) //年龄排序
.map(Student::getName) //提取姓名
.collect(Collectors.toList());//将提取的姓名保存在List中
为了利用多核架构并行执行这段代码,只需要把stream()
替换成parallelStream()
即可。
通过对比两段代码之后,Java8流的方式有几个显而易见的好处。
- 代码是以声明性的方式写的:说明想要做什么(筛选小于14岁的学生)而不是去说明怎么去做(循环、if)
- 将几个基础操作链接起来,表达复杂的数据处理流水线(filter->sorted->map->collect),同时保持代码清晰可读。
总结一下,Java 8的Stream API带来的好处:
- 声明性-更简洁,更易读
- 可复合-更灵活
- 可并行-性能更好
流简介
流到底是什么?简单定义:“从支持数据处理操作的源生成的元素序列”,下面剖析这个定义。
- 元素序列:像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值。集合讲的是数据,流讲的是计算。
- 源:流使用一个提供数据的源,如集合、数组或输入/输出资源。
- 数据处理操作:流的数据处理功能之处类似于数据库的操作,以及函数式编程语言中的常用操作,如filter、map、reduce、find、match、sort等。流的操作可以顺序执行,也可以并行执行。
- 流水线:很多流的操作会返回一个流,这样多个操作就可以链接起来,形成一个流水线。可以看成数据库式查询。
- 内部迭代:于迭代器显示迭代的不同,流的迭代操作是在背后进行的。
看一段代码,更好理解这些概念
List<String> resultName = students.stream() //从列表中获取流
.filter(student -> student.getAge() < 16) //操作流水线:筛选
.map(Student::getName) //操作流水线:提取姓名
.limit(2) //操作流水线:只取2个
.collect(Collectors.toList());//将结果保存在List中
在上面代码中,数据源是学生列表(students),用来给流提供一个元素序列,调用stream()
获取一个流,接下来就是一系列数据处理操作:filter、map、limit和collect。除collect之外,所有这些操作都会返回一个流,组成了一条流水线。最后collect操作开始处理流水线,并返回结果。
流与集合
粗略的说,流与集合之间的差异就在于什么时候进行计算。
- 集合是一个内存中的数据结构(可以添加或者删除),它包含数据结构中目前所有的值——集合中的每个元素都是预先处理好然后添加到集合中的。
- 流则是在概念上固定的数据结构(不能添加或删除元素),其元素是按需计算的。
在哲学中,流被看作在时间中分布的一组值,而集合则是空间(计算机内存)中分布的一组值,在一个时间点上全体存在。
只能遍历一次
和迭代器类似,流只能遍历一次。遍历完成之后,我们说这个流已经被消费掉了。
例如下面的代码会抛出异常
Stream<Student> stream = students.stream();
stream.forEach(System.out::println);
stream.forEach(System.out::println);
执行之后抛出如下异常:
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
at java.util.stream.AbstractPipeline.sourceStageSpliterator(AbstractPipeline.java:279)
at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580)
at com.example.demo.java8.stream.StreamTest.main(StreamTest.java:58)
所以要记得,流只能消费一次。
外部迭代与内部迭代
我们使用iterator或者foreach遍历集合时的这种迭代方式被称为外部迭代,而Streams库使用内部迭代,它帮你把迭代做了,还把得到的流值存在了某个地方,你只要给出一个函数说要干什么就可以了。
下面的代码说明了这种区别。
- 外部迭代
//使用增强for循环做外部迭代,底层还是迭代器
List<String> resultName = new ArrayList<>();
for (Student student : students) {
resultName.add(student.getName());
}
//使用迭代器做外部迭代
Iterator<Student> iterator = students.iterator();
while (iterator.hasNext()){
Student student = iterator.next();
resultName.add(student.getName());
}
- 内部迭代
List<String> resultName = students.stream()
.map(Student::getName)
.collect(Collectors.toList());
流操作
java.util.stream
中的Stream接口定义了许多操作。可以分为两大类。先看一下下面这个例子:
List<String> resultName = students.stream() //从列表中获取流
.filter(student -> student.getAge() < 16) //中间操作
.map(Student::getName) //中间操作
.limit(2) //中间操作
.collect(Collectors.toList());//将Stream转为List
可以看到两类操作:
- filter、map和limit链接的一条流水线
- collect触发流水线执行并关闭它
流水线中流的操作称为中间操作,关闭流的操作称为终端操作
中间操作
诸如filter或sorted等中间操作会返回一个流,这让很多操作链接起来形成一个复合的流水线(查询)。重要的是,除非流水线上触发一个终端操作,否则中间操作不会执行任何处理——它们很懒。
因为中间操作一般都可以合并起来,在终端操作时一次性全部处理。
修改一下上面的代码,看一下发生了什么:
List<String> resultName = students.stream() //从列表中获取流
.filter(student -> {
System.out.println("filter:"+student.getName());
return student.getAge() < 16;
}) //中间操作
.map(student -> {
System.out.println("map:"+student.getName());
return student.getName();
}) //中间操作
.limit(3) //中间操作
.collect(Collectors.toList());//将Stream转为List
执行结果如下:
filter:张初一
map:张初一
filter:李初二
map:李初二
filter:孙初三
map:孙初三
可以发现,利用流的延迟性质实现了几个好的优化。limit操作实现了只选择前三个(一种称为短路的技巧),filter和map操作是相互独立的操作,但他们合并到同一次遍历中(这种技术称为循环合并)。
终端操作
终端操作会从流的流水线生成结果。其结果可以是任何不是流的值,例如List、Integer,亦或是void等。
使用流
流的使用一般包括三件事:
- 一个数据源(如集合)来执行一个查询
- 一个中间操作链,形成一条流水线
- 一个终端操作,执行流水线,并生成结果
流的流水线背后的理念类似于构建器模式。在构建器模式中有一个调用链来设置一套配置(对流来说这就是一个中间操作链),接着时调用build方法(对流来说就是终端操作)。
补充一下示例代码使用的Student
@Data
public class Student {
private String name;
private int age;
private boolean member;
private Grade grade;
public Student() {
}
public Student(String name, int age, boolean member, Grade grade) {
this.name = name;
this.age = age;
this.member = member;
this.grade = grade;
}
public enum Grade{
JUNIOR_ONE,JUNIOR_TWO,JUNIOR_THREE
}
}