Java中的Stream
1. 什么是 Stream?
核心思想: Stream(流)是一个来自数据源(如集合、数组、I/O channel)的元素队列,并支持聚合操作。
你可以把它想象成一个高级的迭代器(Iterator),但有两个根本性的区别:
- 声明式编程: 你只需要指定"做什么",而不是"如何做"。(例如,"过滤出所有长度大于3的字符串"而不是"遍历每个元素,检查其长度,如果大于3则加入一个新列表")。
- 可组合性: Stream 操作可以像流水线一样链接起来,形成一个复杂的处理流程,但只需要一次迭代。
- 内部迭代: 迭代操作在 Stream 库内部自动完成,无需你手动写 for 循环。
1.1 核心特点详解:
- 惰性求值(Lazy Evaluation):
- 中间操作(如 filter、map)不会立即执行
- 只有遇到终止操作(如 collect、forEach)时才会触发计算
- 示例:list.stream().filter(x -> x > 10).count()中,filter 操作在 count 被调用时才执行
- 不可复用(Single Use):
- 每个 Stream 管道(pipeline)只能被消费一次
- 重复使用会抛出 IllegalStateException
- 解决方案:每次需要时重新创建 Stream
- 内部迭代(Internal Iteration):
- 不需要显式编写 for/while 循环
- 迭代过程由 Stream API 内部处理
- 对比:传统 for 循环是外部迭代
- 函数式风格(Functional Style):
- 支持 Lambda 表达式(如 x -> x*2)
- 支持方法引用(如 String::length)
- 无副作用:理想情况下不修改外部状态
1.2 Stream与集合的区别
| 特性 | 集合(Collection) | Stream(流) |
|---|---|---|
| 存储 | 存储实际数据 | 不存储数据 |
| 操作方式 | 外部迭代(foreach循环) | 内部迭代 |
| 数据处理 | 立即执行 | 延迟执行 |
| 可重用性 | 可多次遍历 | 只能遍历一次 |
| 并行能力 | 需要手动实现 | 内置并行支持 |
2. Stream 操作的三个阶段
使用 Stream 通常涉及三个步骤,形成一个"流管道":
-
创建流(Source): 从一个数据源(如集合)创建一个 Stream 对象。
-
中间操作(Intermediate Operations): 对 Stream 进行一系列的处理/转换(如过滤、映射、排序),这些操作返回一个新的 Stream,所以可以链式调用。中间操作是"惰性的",它们不会立即执行,只是在遇到终端操作时构建一个处理流程。
-
终端操作(Terminal Operation): 执行管道并产生结果。执行后,该流就被"消耗"了,不能再使用。结果可以是一个值(如 min, max, count),一个集合(如 collect),或者一个副作用(如 forEach)。
3. 核心操作详解
3.1 创建流
java
// 1. 从集合创建 (最常用)
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream1 = list.stream(); // 顺序流
Stream<String> stream2 = list.parallelStream(); // 并行流
// 2. 从数组创建
String[] array = {"a", "b", "c"};
Stream<String> stream3 = Arrays.stream(array);
// 3. 使用 Stream.of() 静态方法
Stream<String> stream4 = Stream.of("a", "b", "c");
// 4. 创建无限流 (常用于生成序列)
Stream<Integer> stream5 = Stream.iterate(0, n -> n + 2); // 0, 2, 4, 6...
Stream<Double> stream6 = Stream.generate(Math::random); // 无限个随机数
3.2 常见的中间操作
这些操作会返回一个新的 Stream,可以链式调用。
- filter(Predicate): 过滤,保留满足条件的元素。
java
list.stream().filter(s -> s.startsWith("A")); // 保留以"A"开头的字符串
- map(Function<T, R>): 映射,将元素转换成另一种形式。
java
list.stream().map(String::toUpperCase); // 将所有字符串转为大写
list.stream().map(s -> s.length()); // 将字符串流映射为它的长度流(Integer)
- flatMap(Function<T, Stream>): 将每个元素转换成一个流,然后把所有流连接成一个流。用于"打平"嵌套结构。
java
List<List<String>> nestedList = ...;
Stream<String> flatStream = nestedList.stream()
.flatMap(List::stream); // 将多个List打平成单个元素流
- distinct(): 去重。
- sorted() / sorted(Comparator): 排序。
- limit(long maxSize): 限制流的最大长度。
- skip(long n): 跳过前 n 个元素。
3.3 常见的终端操作
这些操作会触发流的执行。
- forEach(Consumer): 遍历每个元素。
java
list.stream().forEach(System.out::println);
- collect(Collector<T, A, R>): 最强大的终端操作,将流转换为其他形式,如 List, Set, Map,或进行复杂的汇总。
java
List<String> newList = list.stream().filter(...).collect(Collectors.toList());
Set<String> set = list.stream().collect(Collectors.toSet());
String joined = list.stream().collect(Collectors.joining(", "));
Map<Integer, List<String>> groupByLength = list.stream().collect(Collectors.groupingBy(String::length));
-
toArray(): 将流转换为数组。
-
reduce(BinaryOperator): 将流中的元素反复结合,得到一个值。
java
Optional<Integer> sum = Stream.of(1, 2, 3).reduce((a, b) -> a + b); // 6
-
min(Comparator) / max(Comparator): 获取最小/最大值。
-
count(): 返回流中元素的个数。
-
anyMatch(Predicate) / allMatch / noneMatch: 短路匹配,检查流中是否有元素匹配给定条件。
java
boolean hasA = list.stream().anyMatch(s -> s.contains("A"));
4. 完整的例子
假设我们有一个 Person 对象的列表,我们想找出所有年龄大于18岁的人的名字,并按字母顺序排序,最后放入一个新的列表。
- 传统方式(命令式):
java
public static void main(String[] args) {
List<Person> people = new ArrayList<>();
List<String> adultNames = new ArrayList<>();
for (Person person : people) {
if (person.getAge() > 18) {
adultNames.add(person.getName());
}
}
Collections.sort(adultNames);
}
- Stream 方式(声明式):
java
public static void main(String[] args) {
List<Person> people = new ArrayList<Person>();
List<String> adultNames = people.stream() // 1. 创建流
.filter(person -> person.getAge() > 18) // 2. 中间操作:过滤年龄
.map(Person::getName) // 3. 中间操作:映射为名字
.sorted() // 4. 中间操作:排序
.collect(Collectors.toList()); // 5. 终端操作:收集为List
}
5. 重要特性与注意事项
-
不存储数据: Stream 本身不存储数据,它只是数据源的视图。
-
不修改源数据: Stream 操作不会修改底层数据源。例如,filter 不会从原集合中删除元素,它会生成一个不含这些元素的新 Stream。
-
惰性执行(Laziness): 中间操作是惰性的,只有在终端操作被调用时,它们才会开始执行。这允许进行一些优化,比如短路操作(findFirst, anyMatch)。
-
一次性使用: 一个 Stream 对象一旦被终端操作消耗,就不能再被使用。如果你需要再次遍历,必须创建一个新的 Stream。
-
并行处理: 并行流(parallelStream)可以自动将工作分配到多个线程上,但并非总是更快。它适用于数据量大、处理耗时的场景,并且要确保操作是无状态和不干扰的。