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 使用分为三步:
- 获取流:从集合、数组等得到
Stream<T>。 - 中间操作(可多个):如
filter、map、sorted,返回新的 Stream,不会立即执行,只是组装流水线。 - 终端操作:如
forEach、collect、reduce,触发实际计算并消耗流,得到结果或副作用。
惰性求值:只有遇到终端操作时,中间操作才会按需执行;若没有终端操作,中间操作不会执行。
// 无终端操作:不会打印 "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
常见创建方式如下。
从集合创建
import java.util.*;
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream(); // 顺序流
Stream<String> parallel = list.parallelStream(); // 并行流(多线程)从数组创建
String[] arr = { "x", "y", "z" };
Stream<String> s1 = Arrays.stream(arr);
// 基本类型数组有对应 Stream,避免装箱
int[] ints = { 1, 2, 3 };
IntStream intStream = Arrays.stream(ints);静态工厂方法
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>) | 对每个元素执行副作用,常用于调试 |
示例:过滤、映射、去重、排序。
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
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
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 归约
// 求和
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 可以把内层流合并成一个流。
// 句子列表 → 拆成单词流
List<String> sentences = Arrays.asList("Hello World", "Java Stream");
Stream<String> words = sentences.stream()
.flatMap(s -> Arrays.stream(s.split(" ")));
// "Hello", "World", "Java", "Stream"// 列表的列表 → 摊平为单个元素流
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() |
list.stream()
.map(String::length)
.filter(n -> n > 2)
.forEach(System.out::println);完整示例:从集合到结果
下面示例:从用户名字列表中过滤非空、去重、按长度排序、取前 3 个并收集为 List。
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。如需多次使用,请重新从数据源获取流。
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() 会使用多线程,适合数据量大、操作耗时的场景;数据量小或操作简单时,顺序流往往更简单、可预测。使用并行流时需注意共享变量的线程安全。
相关链接
- Lambda 表达式 — Stream 中大量使用 Lambda 与函数式接口
- Optional —
findFirst、max等返回 Optional - List、Set、Map 与遍历 — 数据源与遍历方式
- Oracle Java 教程 - Stream