Java Stream流式思想

Posted 李亦华的博客

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java Stream流式思想相关的知识,希望对你有一定的参考价值。

说到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循环的循环体才是“做什么”

为什么使用循环?因为要进行遍历。但循环是遍历的唯一方式吗?遍历是指每一个元素逐一进行处理,而并不是从第一个到最后一个顺次处理的循环。前者是目的,后者是方式。

试想一下,如果希望对集合中的元素进行筛选过滤:

  1. 将集合A根据条件一过滤为子集B;
  2. 然后再根据条件二过滤为子集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流的固有印象!

整体来看,流式思想类似于工厂车间的“生产流水线”。

img

当需要对多个元素进行操作(特别是多步操作)的时候,考虑到性能及便利性,我们应该首先拼好一个“模型”步骤 方案,然后再按照方案去执行它。

img

这张图中展示了过滤、映射、跳过、计数等多步操作,这是一种集合元素的处理方案,而方案就是一种“函数模型”。图中的每一个方框都是一个“流”,调用指定的方法,可以从一个流模型转换为另一个流模型。而最右侧的数字 3是最终结果。

这里的 filter 、 map 、 skip 都是在对函数模型进行操作,集合元素并没有真正被处理。只有当终结方法 count 执行的时候,整个模型才会按照指定策略执行操作。而这得益于Lambda的延迟执行特性

备注:“Stream流”其实是一个集合元素的函数模型,它并不是集合,也不是数据构,其本身并不存储任何元素(或其地址值)。

Stream(流)是一个来自数据源的元素队列

  • 元素是特定类型的对象,形成一个队列。 Java中的Stream并不会存储元素,而是按需计算。
  • 数据源:流的来源。 可以是集合,数组等。

和以前的Collection操作不同, Stream操作还有两个基础的特征:

  • Pipelining:中间操作都会返回流对象本身。 这样多个操作可以串联成一个管道, 如同流式风格(fluent style)。 这样做可以对操作进行优化, 比如延迟执行(laziness)和短路( short-circuiting)。
  • 内部迭代:以前对集合遍历都是通过Iterator或者增强for的方式,显式的在集合外部进行迭代,这叫做外部迭代。 Stream提供了内部迭代的方式,流可以直接调用遍历方法。

当使用一个流的时候,通常包括三个基本步骤:获取一个数据源(source)→ 数据转换→执行操作获取想要的结果,每次转换原有Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以 像链条一样排列,变成一个管道。

以上是关于Java Stream流式思想的主要内容,如果未能解决你的问题,请参考以下文章

流式计算-Java8Stream

流式计算-Java8Stream

Stream流 方法引用 - 16

java_Stream 流式计算

Java8 流式 API(`java.util.stream`)

Java8 Stream流式编程