# Stream API

Stream 是 Java 8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作。

当你使用 Stream 时,你会通过 3 个阶段来建立一个操作流水线。

# 阶段 说明
1 生成 创建一个 Stream 。
2 操作 在一个或多个步骤中,指定将初始 Stream 转换为另一个 Stream 的中间操作。
3 数据收集 使用一个终止操作来产生一个结果。该操作会强制它之前的延迟操作立即执行。在这之后,该 Stream 就不会再被使用了。

例如:

String[] array = new String[]{"hello", "world", "goodbye"};
Stream<String> stream = 
List<String> list = Arrays
    .stream(array)                  // 1. 生成
    .map(String::toUpperCase)       // 2. 操作
    .collect(Collectors.toList());  // 3. 数据收集

# 生成:创建 Stream

# 由集合&数组生成 Stream

Stream 作为元素的『序列』,自然而然,我们想到通过集合、数组生成 Stream 。

Java 8 在 Collection 接口中新添加了 .stream 方法,可以将任何集合转化为一个 Stream 。

List<String> list = new ArrayList<>();
...

Stream<String> stream = list.stream();

如果你面对的是一个数组,也可以用静态的 Stream.of 方法将它转化为一个 Stream 。

String[] array = new String[]{"hello", "world", "goodbye"};
Stream<String> stream = Stream.of(array);

Stream.of 方法有一个重载形式,接收可变长度的参数:

Stream.of(xxx, xxx, xxx, xxx);
// 本质上等同于
stream = Stream.of(array);

另外,JDK 的 Arrays 工具类中也提供了一个 .stream 方法用以将数组转化为 Stream 。

特别地,当数组元素类型 T 是原始类型,静态方法 .stream(T[]) 将返回原始类型的 Stream :

IntStream stream = Arrays.stream(array);

# 直接创建 Stream

除了由集合和数组生成 Stream,Stream API 为 Stream 提供了静态方法 .generate(Supplier) 方法、 .iterator(Final T, final UnaryOperator<T> f) 方法,直接创建 Stream 。

这里需要注意的是,上一章节中,通过数组&集合生成的 Stream 对象中的数据的数量是确定的,即为数组和集合中的数据的数量。 而通过 .generate.iterator 方法生成的 Stream 对象中的数据的数量是无限的,即,你向 Stream 对象每次『要』一个对象时它都会每次生成一个返回给你,从而达到『无限个』的效果。

通常直接创建的 Stream 对象会结合 .limit() 方法使用。

Stream.generate(Supplier) 通过参数 Supplier 获取 Stream 序列的新元素。

Stream<Double> stream = Stream.generate(Math::random).limit(10);

.iterator 方法要求你提供两个参数:

  • 数据序列中的第一个数。这个数字需要使用者认为指定,通常也被称为『种子』。

  • 根据『前一个数字』计算『下一个数字』的计算规则。

Stream<Integer> stream = Stream.iterate(1, n -> n += 2);
// 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, ...

整个序列的值就是:x, f(x), f(f(x)), f(f(f(x))), ...

# 中间操作:处理 Stream 中的数据序列

所谓的中间操作,指的就是对 Stream 中的数据使用流转换方法。

filter 和 map 方法是 stream 最常见的两个流转换方法。

# filter 方法

.filter 是过滤转换,它将产生一个新的流,其中『包含符合某个特定条件』的所有元素。逻辑上就是『选中』的功能。

例如:

String[] array = new String[]{"hello", "world", "goodbye"};

Stream<String> stream = Arrays.stream(array);
Stream<String> newStream = stream.filter((item) -> item.startsWith("h"));

System.out.println(newStream.count());  // 1

.filter 方法的参数是一个 Predicate<T> 对象——即一个从 T 到 boolean 的函数。

# map 方法

我们经常需要对一个流中的值进行某种形式的转换。这是可以考虑使用 .map 方法,并传递给它一个负责进行转换的函数。

例如:

newStream = stream.map(String::toUpperCase);

# anyMatch / allMatch / noneMatch 判断

anyMatch / allMatch / noneMatch 三个方法用于判断 Stream 中的元素是否『至少有一个』、『全部都』、『全部都不』满足某个条件。因此,这三个方法的返回值都是 boolean 值。

boolean b = stream.anyMatch((item) -> item.length() == 5);

# 数据收集

当经过第 2 步的操作之后,你肯定会需要查看或收集流中(经过处理)的数据。

# iterator 方法

Stream 的 iterator 方法会返回一个传统风格的迭代器,用于访问元素。

Iterator<String> it = stream.iterator();
while (it.hasNext()) {
    String str = it.next();
    System.out.println(str);
}

# toArray 方法

Stream 的 .toArray 方法会返回一个包含了流中所有元素的数组。

Object[] array = stream.toArray();

不过, .toArray 方法返回的是一个 Object[] 数组。如果想获得一个正确类型的数组,可以将数组类型的构造函数传递给它:

String[] array = stream.toArray(String[]::new); // 注意,这里是 String[] 而不是 String

# collect:收录到集合

如果你想将 Stream 中的数据收录到集合中,那么你可以使用 .collect 方法。

不过最原始最底层的 .collect 方法看起来比较『奇怪』:

stream.collect(HashSet::new, HashSet::add, HashSet::addAll);
stream.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);

这里的第一第二个参数好理解,比较奇怪的是第三个参数。这里需要用到集合的 .addAll 方法是因为会将 Stream 中的数据先存放于多个集合中,最后再将多个集合合并成一个总的集合中再返回(这种『奇怪』的行为是和 Stream API 的并发特性有关)

为此,Stream 提供了几个重载的 .collect 方法简化使用:

stream.collect(Collectors.toCollection())
stream.collect(Collectors.toList());
stream.collect(Collectors.toSet())

# collect:收录到 Map

假设你有一个 Stream<Student>,并且你想将其中的元素收集到一个 map 中,这样你随后可以通过他们的 ID 来进行查找。为了实现这个目的,你可以再 .collect 方法中使用 Collectors.toMap

Collectors.toMap 有两个函数参数,分别用来生成 map 的 key 和 value。例如:

Map<Integer, String> map = stream.collect(
    Collectors.toMap(Student::getId, Student::getName)
);

一般来说,Map 的 value 通常会是 Student 元素,这样的话可以实使用 Function.identity() 作为第二个参数,来实现这个效果。(你可以将它视为固定用法)。

Map<Integer, Student> map = stream.collect(
    Collectors.toMap(Student::getId, Function.identity())
);