Java8 Stream API之流简介

Java8 Stream API之流简介

Symon 885 2020-11-13

流是什么

流是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
    }
}

# java # java8