Skip to content

Stream API

概述

Stream API 是 JDK 8 引入的一套对「数据序列」进行声明式处理的 API,常用于集合的过滤、转换、聚合等操作。它不直接存储数据,而是描述「从哪来、经过什么操作、得到什么结果」,配合 Lambda 表达式 和方法引用,可以写出简洁、可读性强的集合处理代码。

Stream 与其它「流」的区别

  • 集合(Collection):存储元素的数据结构,可多次遍历。
  • IO 流(InputStream/OutputStream 等):与磁盘、网络等 I/O 相关的字节/字符流,和本节的 Stream 无直接关系。
  • Stream API 的 Stream:对数据源的一次性的、可链式调用的计算流水线,不修改原数据源,且通常只消费一次。

前置建议

已掌握 Lambda 表达式集合的 List/Set/Map泛型。Stream 大量使用函数式接口与泛型。


核心概念:数据源 → 中间操作 → 终端操作

一次典型的 Stream 使用分为三步:

  1. 获取流:从集合、数组等得到 Stream<T>
  2. 中间操作(可多个):如 filtermapsorted,返回新的 Stream,不会立即执行,只是组装流水线。
  3. 终端操作:如 forEachcollectreduce触发实际计算并消耗流,得到结果或副作用。

惰性求值:只有遇到终端操作时,中间操作才会按需执行;若没有终端操作,中间操作不会执行。

java
// 无终端操作:不会打印 "filter" 或 "map"
list.stream()
    .filter(x -> { System.out.println("filter"); return true; })
    .map(x -> { System.out.println("map"); return x; });
// 加上终端操作才会执行
list.stream().filter(x -> true).map(x -> x).forEach(System.out::println);

创建 Stream

常见创建方式如下。

从集合创建

java
import java.util.*;

List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream();           // 顺序流
Stream<String> parallel = list.parallelStream(); // 并行流(多线程)

从数组创建

java
String[] arr = { "x", "y", "z" };
Stream<String> s1 = Arrays.stream(arr);

// 基本类型数组有对应 Stream,避免装箱
int[] ints = { 1, 2, 3 };
IntStream intStream = Arrays.stream(ints);

静态工厂方法

java
Stream<String> s = Stream.of("hello", "world");
Stream<Object> empty = Stream.empty();
Stream<Integer> infinite = Stream.iterate(0, n -> n + 1); // 无限流,需 limit 等截断
Stream<Double> random = Stream.generate(Math::random);

常用中间操作

中间操作返回 Stream,可链式调用;不执行计算,只描述「经过什么步骤」。

方法说明
filter(Predicate<T>)保留使条件为 true 的元素
map(Function<T, R>)每个元素转换为另一个值
flatMap(Function<T, Stream<R>>)每个元素映射为一个流,再「摊平」成一个流
distinct()去重(按 equals)
sorted() / sorted(Comparator)排序
limit(long n)最多保留前 n 个
skip(long n)跳过前 n 个
peek(Consumer<T>)对每个元素执行副作用,常用于调试

示例:过滤、映射、去重、排序。

java
import java.util.*;
import java.util.stream.*;

List<String> names = Arrays.asList("Tom", "Jerry", "Spike", "Tom", "Tyke");

List<String> result = names.stream()
    .filter(name -> name.length() > 3)   // 保留长度 > 3
    .map(String::toUpperCase)            // 转大写,方法引用
    .distinct()                          // 去重
    .sorted()                            // 字典序
    .limit(10)
    .collect(Collectors.toList());       // 终端操作:收集为 List

// result: [JERRY, SPIKE, TOM, TYKE]

常用终端操作

终端操作触发计算,流被消耗,不能再次使用。

方法说明
forEach(Consumer<T>)对每个元素执行操作,无返回值
collect(Collector)将结果收集到 List、Set、Map 等
reduce(...)归约成单个值(如求和、拼接)
count()元素个数
findFirst() / findAny()返回 Optional 的第一个/任意一个
anyMatch / allMatch / noneMatch(Predicate)是否存在/全部/全部不满足条件
min(Comparator) / max(Comparator)最小/最大(返回 Optional)

示例 1:forEach 与 count

java
List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5);
nums.stream().filter(n -> n % 2 == 0).forEach(System.out::println); // 2 4
long cnt = nums.stream().filter(n -> n % 2 == 0).count();          // 2

示例 2:collect 与 Collectors

java
import java.util.stream.*;

List<String> list = Arrays.asList("a", "bb", "ccc");

// 收集为 List
List<String> asList = list.stream().filter(s -> s.length() > 1).collect(Collectors.toList());

// 收集为 Set(去重)
Set<String> asSet = list.stream().collect(Collectors.toSet());

// 拼接字符串
String joined = list.stream().collect(Collectors.joining(", "));  // "a, bb, ccc"

// 按长度分组
Map<Integer, List<String>> byLength = list.stream()
    .collect(Collectors.groupingBy(String::length));
// 1 -> [a], 2 -> [bb], 3 -> [ccc]

提示

JDK 16+ 可直接用 stream.toList() 得到不可变 List,无需 Collectors.toList()

示例 3:reduce 归约

java
// 求和
int sum = Arrays.asList(1, 2, 3, 4, 5).stream()
    .reduce(0, Integer::sum);  // 0 为初始值,Integer::sum 为 (a,b) -> a+b

// 拼接字符串
String concat = Stream.of("A", "B", "C").reduce("", (a, b) -> a + b);

flatMap:一对多再摊平

当「一个元素对应多个结果」时,用 map 会得到 Stream<Stream<R>>;用 flatMap 可以把内层流合并成一个流。

java
// 句子列表 → 拆成单词流
List<String> sentences = Arrays.asList("Hello World", "Java Stream");

Stream<String> words = sentences.stream()
    .flatMap(s -> Arrays.stream(s.split(" ")));
// "Hello", "World", "Java", "Stream"
java
// 列表的列表 → 摊平为单个元素流
List<List<Integer>> listOfLists = Arrays.asList(
    Arrays.asList(1, 2),
    Arrays.asList(3, 4)
);
List<Integer> flat = listOfLists.stream()
    .flatMap(List::stream)
    .collect(Collectors.toList());
// [1, 2, 3, 4]

方法引用在 Stream 中的使用

Lambda 的常见简写是方法引用,在 Stream 中非常常用:

写法说明
String::length实例方法,等价于 s -> s.length()
System.out::println实例方法,等价于 x -> System.out.println(x)
Integer::parseInt静态方法,等价于 s -> Integer.parseInt(s)
String::toUpperCase无参实例方法,等价于 s -> s.toUpperCase()
java
list.stream()
    .map(String::length)
    .filter(n -> n > 2)
    .forEach(System.out::println);

完整示例:从集合到结果

下面示例:从用户名字列表中过滤非空、去重、按长度排序、取前 3 个并收集为 List。

java
import java.util.*;
import java.util.stream.*;

public class StreamExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Tom", "", "Jerry", "Tom", "Spike", null);

        List<String> top3 = names.stream()
            .filter(Objects::nonNull)      // 去掉 null
            .filter(s -> !s.isEmpty())    // 去掉空串
            .distinct()
            .sorted(Comparator.comparingInt(String::length))
            .limit(3)
            .collect(Collectors.toList());

        System.out.println(top3);  // [Tom, Jerry, Spike]
    }
}

注意事项

流只能消费一次

Stream 实例被终端操作消费后就不能再使用,再次调用会抛出 IllegalStateException。如需多次使用,请重新从数据源获取流。

java
Stream<String> s = list.stream();
s.forEach(System.out::println);
s.count();  // 错误:流已关闭

空指针与 null 元素

若数据源或中间结果可能含 null,要在 filter 中显式排除(如 Objects::nonNull),否则后续 map 等方法可能抛出 NPE。

何时用 Stream

适合:多步过滤、转换、分组、统计等「声明式」集合处理。简单遍历或单步操作用传统 for 或 集合的 forEach 即可,不必强行用 Stream。

并行流

parallelStream()stream().parallel() 会使用多线程,适合数据量大、操作耗时的场景;数据量小或操作简单时,顺序流往往更简单、可预测。使用并行流时需注意共享变量的线程安全。


相关链接

基于 VitePress 构建