【后端之旅】Java Stream 全解

393 阅读6分钟

Stream (流)作为 Java 8 提供的新特性,极大的方便了开发者进行各种集合操作。流是整个 Java 知识体系的重点,我们没有理由不掌握它。

前言

我们曾经学习过了 Java ListJava MapJava Set 等集合结构,知道可以使用 for 循环控制语句、iterator() 迭代方法来遍历集合中的全部元素。现在存在的问题是这些遍历方法太容易兜圈、跳转,不符合我们大脑的直线思维习惯。Stream 就是为了解决这个痛点而生的。

生成 Stream

在执行各种操作之前,我们需要有一个 Stream。

  • 使用 Collection 提供的 stream() 实例方法或 parallelStream() 实例方法获取流
// 由 java.util.Collection 接口提供
List<String> list = new ArrayList<>();  
Stream<String> stream = list.stream();

// 获取一个并行流
Stream<String> parallelStream = list.parallelStream();
  • 使用 Arrays 提供的 stream 静态方法,用于将普通数组转换为流
// 由 java.util.Arrays 类提供
Integer[] ns = {1, 3, 5, 7, 9};
Stream<Integer> stream = Arrays.stream(ns);
  • 使用 Stream 提供的静态方法,可将零散的数据转换为流
// 由 java.util.stream.Stream 接口提供
Stream<Integer> stream = Stream.of(1, 3, 5, 7, 9);

// 获取一个无限迭代流:1,3,5,7,9,11,...
Stream<Integer> infiniteStream = Stream.iterate(1, x -> x + 2);

// 获取一个无限生成流:"Hello, World!","Hello, World!","Hello, World!",...
Stream<String> generatedSteam = Stream.generate(() -> "Hello, World!");
  • 使用 BufferedReader 提供的实例方法,可获得一个文本文件的段落流
// 由 java.io.BufferedReader 类提供
BufferedReader reader = new BufferedReader(new FileReader("F:\\filename.txt"));  
Stream<String> lineStream = reader.lines();
  • 使用 Pattern 提供的实例方法,可对字符串进行分割并获得字符串流
// 由 java.util.regex.Pattern 类提供
Pattern pattern = Pattern.compile("-");  
Stream<String> stringStream = pattern.splitAsStream("how-are-you-?");
  • 使用 Stream 提供的静态方法,可以合并两个或多个流
Stream<Integer> stream1 = Stream.of(1, 3, 5, 7, 9);
Stream<Integer> stream2 = Stream.of(2, 4, 6, 8, 10);

Stream<Integer> s = Stream.concat(stream1, stream2);

终止 Stream

Stream 生成后,不会自发启动(执行)的。要启动一个 Stream,就需要有一个终止操作(通常放置于流的末尾)。

  • 短路终止(可以提前得到结果,不一定完整遍历上游给的全部元素)
Stream<Integer> stream = Stream.of(1, 3, 5, 7, 9);

# 任何一个元素匹配则返回 true,否则返回 false
stream.anyMatch(n -> n == 3);

# 任何一个元素不匹配则返回 false,否则返回 true
stream.allMatch(n -> n % 2 == 1);

# 任何一个元素匹配则返回 false,否则返回 true
stream.noneMatch(n -> n > 10);

# 返回第一个元素(即一个 Optional)。如果 Stream 中没有元素,则返回 Optional#empty()
stream.findFirst();

# 主要应用于并行流,在串行流中等同于 findFirst()
# 返回一个 Optional(其值是 Stream 中的一个元素)。如果 Stream 中没有元素,则返回Optional#empty()
stream.findAny();
  • 正常终止(不出错的情况下会遍历上游给的全部元素)
Stream<Integer> stream = Stream.of(1, 3, 5, 7, 9);

# 调用 action 函数处理每一个元素,但在并行流中顺序不确定
stream.forEach(n -> System.out.println(n));

# 无论在并行流还是序列流,均按照元素到达的顺序执行 action 函数
stream.forEachOrdered(n -> System.out.println(n));

# 规约。有三种参数形式:
# 这里的 action 首次执行时,acc 为第一个元素,cur 为第二个元素
Optional result1 = stream.reduce((acc, cur) -> acc + cur);
# 这里的 action 首次执行时,acc 为聚合的第一个参数,cur 为第一个元素
Optional result2 = stream.reduce(0, (acc, cur) -> acc + cur);
# 本方法在串行流中与第二种 reduce 重载方法一样
# 而在并行流中第三个参数会将多个线程的执行结果进行合并处理
Optional result2 = stream.reduce(0, (acc, cur) -> acc + cur, (sum1, sum2) -> sum1 + sum2);

# 收集。将全部元素封装到一个特定的数据结构中,其中 Collectors 包括但不限于:
# Collectors.toList()
# Collectors.toSet()
# Collectors.toMap(keyMapper, valueMapper)
# Collectors.joining(divider, prefix, suffix)
# Collectors.counting()
# ...
List<Integer> list = stream.collect(Collectors.toList());

# 获取全部元素中的最小值
# 对于基本类型的流,如 IntStream、LongStream 和 DoubleStream,则不需要比较器
Optional resultMin = stream.min(Integer::compareTo);

# 获取全部元素中的最大值
# 对于基本类型的流,如 IntStream、LongStream 和 DoubleStream,则不需要比较器
Optional resultMax = stream.max(Integer::compareTo);

# 获取全部元素的个数
long n = stream.count();
  • 转换终止

toArray() 是一种比较特殊的终止操作,它的内部实现没有实现 TerminalOp 接口,因此不放在正常终止类别里。

Stream<Integer> stream1 = Stream.of(1, 3, 5, 7, 9);
IntStream stream2 = IntStream.of(1, 3, 5, 7, 9);

# stream1 的类型为 Stream,toArray 方法需要指定返回值的创建函数,否则默认为 Object[]
Integer[] arr1 = stream1.toArray(Integer[]::new);

# stream2 的类型为 IntStream,因此 toArray 方法不需要参数类指定返回值类型
int[] arr2 = stream2.toArray();

过程 Stream

顾名思义,这一节介绍的方法都是在流创建和流终止之间执行各种操作用的。主要功能就是处理流中的每一个元素,或转换,或筛选,或消费。

  • 转换操作
Stream<Integer> stream = Stream.of(1, 3, 5, 7, 9);
Stream<String> strStream = Stream.of("how", "are", "you");

# 处理每一个上游元素,并将返回结果作为下游的元素
stream.map(n -> n * 2)

# 处理每一个上游元素,每次处理返回一个流,然后将该流的元素(可能有多个)一个一个地往下游塞
# 也可以理解为处理全部上游元素后,将所有处理返回的流合并为一个
strStream.flatMap(str -> Arrays.stream(str.split("")))

# 等待上游的全部元素到齐后,进行排序,再将排好序的元素逐一发往下游
stream.sorted(Comparator.reverseOrder());
  • 筛选操作
Stream<Integer> stream = Stream.of(1, 3, 5, 7, 9);

# 元素处理后若返回 true 则该元素进入下游,返回 false 则元素被抛弃
stream.filter(n -> n > 3)

# 只允许指定个数的元素前往下游,后面的全部抛弃
stream.limit(3)

# 抛弃指定个数的元素后,才允许后面的元素前往下游
stream.skip(3)

# 记录已经前往下游的元素,后续有相等的元素到达后则会被抛弃
stream.distinct()
  • 消费操作
Stream<Integer> stream = Stream.of(1, 3, 5, 7, 9);

# 使用上游的元素执行 action 函数,但不返回结果,前往下游的还是原来的元素
# 由于其特性,该方法常用于调试 stream
Stream.peek(n -> System.out.println("num = " + n));

RxJava

RxJava 的思维源自于 Java Stream。所以掌握了 Java Stream 流的概念,再去学习 RxJava 的异步可观察序列就非常简单了:

import rx.Observable;

Observable.from(new String[]{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"})
    .skip(3)
    .take(5)
    .map(s -> s + " 1")
    .subscribe(s -> System.out.println(s));

可以看到,这里的 Observable.from() 充当了序列的创建者,subscribe() 充当了终结者(也是启动者),而 skip()take()map() 则作为中间件对上游的元素分别进行处理然后发往下游。这思维模式就和 Java Stream 如出一辙啊!

小结

本文梳理了一遍 Stream 接口的各个基本方法。有了这样的基础知识,开发者就可以对集合进行各种复杂的处理与计算了。至于更复杂的 flatMapToInt、flatMapToLong、mapToDouble 等方法由于使用频率较低,请您在需要时再另行查阅。