Stream API 是 Java 8 最重要的特性之一。优雅的链式调用背后,惰性求值、Spliterator、并行流陷阱你都理解了吗?
一、Stream 的两种操作
java
List<Integer> result = list.stream()
.filter(n -> n > 0) // 中间操作: 过滤
.map(n -> n * 2) // 中间操作: 映射
.sorted() // 中间操作: 排序
.limit(10) // 中间操作: 截取
.collect(Collectors.toList()); // 终端操作: 收集
| 操作类型 | 特点 | 示例 |
|---|---|---|
| 中间操作 | 惰性求值,返回新 Stream | filter, map, sorted, limit |
| 终端操作 | 触发执行,返回结果 | collect, reduce, count, forEach |
惰性求值示例:
Stream.of(1, 2, 3, 4, 5)
.peek(n -> System.out.println("filter: " + n)) // 打印经过的元素
.filter(n -> n > 2)
.peek(n -> System.out.println("map: " + n))
.map(n -> n * 10)
.findFirst(); // 终端操作触发
输出:
filter: 1
filter: 2
filter: 3
map: 3
只处理到第一个满足条件的元素! 后面的 4, 5 不会处理
这就是"惰性求值" --- 直到终端操作才开始,且只处理必要的元素
二、Stream 管道构建
Stream 管道 (Pipeline) 结构:
┌─────────────────────────────────────────────────┐
│ Stream Pipeline │
│ │
│ Head ──▶ Stage1 ──▶ Stage2 ──▶ Stage3 ──▶ Terminal │
│ (filter) (map) (sorted) (collect) │
│ │
│ 每个 Stage 是一个 Sink (水槽模式) │
│ 数据从 Head 流向 Terminal │
│ │
│ Sink 接口: │
│ ├── begin(size) → 开始处理前 │
│ ├── accept(value) → 处理一个元素 │
│ ├── end() → 处理完毕 │
│ ├── cancellationRequested() → 是否可以短路 │
│ └── ... │
└─────────────────────────────────────────────────┘
java
// Stream 内部结构 (简化)
abstract class AbstractPipeline {
private AbstractPipeline upstream; // 上游 Stage
private AbstractPipeline nextStage; // 下游 Stage
private int depth; // 管道深度
}
abstract class ReferencePipeline {
// filter 实现
public Stream filter(Predicate predicate) {
return new StatelessOp(this, StreamShape.REFERENCE,
StreamOpFlag.NOT_SIZED) {
// 包装 Sink
Sink opWrapSink(int flags, Sink downstream) {
return new Sink.ChainedReference(downstream) {
public void accept(Object u) {
if (predicate.test(u))
downstream.accept(u); // 满足条件才传递给下游
}
};
}
};
}
}
Sink 包装链
终端操作触发时,所有中间操作倒序包装成 Sink 链:
调用链: filter → map → sorted → collect
构建链路 (从终端操作向上游构建):
collect (TerminalSink)
↑ 包装
sorted (SortedSink)
↑ 包装
map (MappingSink)
↑ 包装
filter (FilteringSink)
↑
数据源 → 从这里开始逐元素处理
处理一个元素:
FilteringSink.accept(e)
├── 不满足条件 → 丢弃
└── 满足条件 → MappingSink.accept(e)
├── 转换 e → e'
└── SortedSink.accept(e')
├── 放入缓冲区
└── end() 时排序 → TerminalSink.accept(e'')
三、短路操作
java
// 短路操作: 找到满足条件的第一个就停止
Optional<Integer> first = Stream.of(1, 2, 3, 4, 5)
.filter(n -> n > 3)
.findFirst(); // 只处理 1,2,3,4 → 找到 4 就停止
// 内部机制: Sink.cancellationRequested()
// findFirst 的 TerminalSink 在 accept 后返回 true
// 上游 Sink 检查到 cancellationRequested → 停止处理
// 短路操作列表:
// findFirst, findAny, anyMatch, allMatch, noneMatch, limit
短路执行流程:
数据: [1, 2, 3, 4, 5]
操作: filter(n > 3) → findFirst()
FilteringSink TerminalSink
│ │
1 → accept(1) │
└─ 1 > 3? 否 │
2 → accept(2) │
└─ 2 > 3? 否 │
3 → accept(3) │
└─ 3 > 3? 否 │
4 → accept(4) │
└─ 4 > 3? 是 │
└─▶ accept(4) │ ← 找到了!
│ cancellationRequested = true
5 → accept(5) │
└─ cancel! 停止 │
四、Spliterator:Stream 的数据源
java
// Spliterator 是 Stream 的迭代器分割器
// "Split" + "Iterator" = 可分割的迭代器
public interface Spliterator<T> {
// 尝试处理剩余元素,返回 false 表示已处理完
boolean tryAdvance(Consumer<? super T> action);
// 尝试分割为两个独立的 Spliterator (用于并行流)
Spliterator<T> trySplit();
// 估算剩余元素数量
long estimateSize();
// 特征位
int characteristics();
}
// 特征位:
// ORDERED - 有序
// DISTINCT - 元素唯一
// SORTED - 已排序
// SIZED - 大小已知
// NONNULL - 元素非 null
// IMMUTABLE - 不可变
// CONCURRENT - 并发安全
// SUBSIZED - 子 Spliterator 也是 SIZED
不同数据源的 Spliterator:
ArrayList: ArrayListSpliterator (SIZED + SUBSIZED + ORDERED)
→ 知道大小, 可高效分割
LinkedList: LinkedListSpliterator (ORDERED)
→ 不知道大小, 分割效率低
HashSet: HashSetSpliterator (DISTINCT + SIZED)
→ 无序
Range: RangeIntSpliterator (SIZED + SORTED)
→ 大小已知, 已排序
Iterator: IteratorSpliterator
→ 通用包装, 性能最差
并行流的分割过程
并行流 Spliterator 分割:
初始 Spliterator (1-100, 100个元素)
│
│ trySplit()
▼
┌────┴────┐
│ 1-50 │ 51-100
│ │
│ trySplit() trySplit()
▼ ▼
┌─┴─┐ ┌─┴──┐
│1-25│ │51-75│
│26-50│ │76-100│
ForkJoinPool 分配任务:
Thread1: 处理 1-25
Thread2: 处理 26-50
Thread3: 处理 51-75
Thread4: 处理 76-100
五、并行流(Parallel Stream)
java
// 串行流
list.stream().filter(n -> n > 0).collect(Collectors.toList());
// 并行流
list.parallelStream().filter(n -> n > 0).collect(Collectors.toList());
// 串行流转并行流
list.stream().parallel().filter(n -> n > 0).collect(Collectors.toList());
并行流原理
java
// 并行流底层使用 ForkJoinPool.commonPool()
public final class ForkJoinPool {
// 默认并行度: CPU 核心数 - 1
static final int COMMON_PARALLELISM =
Runtime.getRuntime().availableProcessors() - 1;
}
// 并行流执行流程:
// 1. 数据源 Spliterator.trySplit() 分割
// 2. 每个子任务提交到 ForkJoinPool
// 3. 每个 worker 线程独立执行 Stream Pipeline
// 4. 结果合并 (reduce/collect)
并行流执行:
ForkJoinPool (4 threads)
任务分割: 结果合并:
┌──────────────────┐ ┌──────────────────┐
│ [1-250] filter+map│ │ 合并结果1 │
│ [251-500] │──fork───▶│ 合并结果2 │
│ [501-750] │ │ 合并结果3 │
│ [751-1000] │ │ 合并结果4 │
└──────────────────┘ └──────────┬───────┘
│
最终结果
并行流陷阱
java
// 陷阱1: 共享可变状态 (线程不安全!)
List<Integer> results = new ArrayList<>();
IntStream.range(0, 10000).parallel().forEach(i -> {
results.add(i); // ArrayList 非线程安全!
});
// 结果可能丢失元素, 甚至抛异常
// 修复: 使用线程安全容器或 collect
List<Integer> results = IntStream.range(0, 10000).parallel()
.boxed()
.collect(Collectors.toList()); // 线程安全
// 陷阱2: 顺序敏感操作
// 并行流 + limit 不保证顺序
Stream.of(1,2,3,4,5,6,7,8).parallel()
.limit(3)
.collect(Collectors.toList());
// 结果不确定! 可能是 [1,3,5] 或其他
// 修复: 使用 forEachOrdered 或确保 ORDERED 特征
并行流的性能陷阱
java
// 陷阱3: 小数据量并行更慢
List<Integer> small = IntStream.range(0, 100).boxed().collect(Collectors.toList());
// ❌ 小数据量用并行流 → 分割+线程切换开销 > 计算开销
small.parallelStream().map(n -> n * 2).collect(Collectors.toList());
// ✅ 大数据量才适合并行
List<Integer> large = IntStream.range(0, 10_000_000).boxed().collect(Collectors.toList());
large.parallelStream().map(n -> n * 2).collect(Collectors.toList());
| 数据量 | 串行流 | 并行流 | 建议 |
|---|---|---|---|
| < 1000 | 1ms | 5ms | 串行 |
| 1000-10000 | 5ms | 8ms | 串行 |
| 10000-100000 | 50ms | 30ms | 看场景 |
| > 100000 | 500ms | 150ms | 并行 |
java
// 陷阱4: 共享 ForkJoinPool
// 所有 parallelStream 共享同一个 ForkJoinPool.commonPool()
// 一个任务阻塞 → 影响全局
// ❌ 某个并行流任务很慢,会占用公共线程池
list1.parallelStream().forEach(n -> Thread.sleep(1000));
// 同时执行的其他 parallelStream 会被影响
// ✅ 使用自定义 ForkJoinPool
ForkJoinPool customPool = new ForkJoinPool(4);
customPool.submit(() ->
list.parallelStream().forEach(n -> n * 2)
).get();
六、collect 原理
java
// collect 是最常用的终端操作
list.stream()
.filter(n -> n > 0)
.collect(Collectors.toList());
// Collector 接口
public interface Collector<T, A, R> {
Supplier<A> supplier(); // 创建结果容器
BiConsumer<A, T> accumulator(); // 元素累加到容器
BinaryOperator<A> combiner(); // 合并两个容器(并行)
Function<A, R> finisher(); // 容器→最终结果
Set<Characteristics> characteristics(); // 特征
}
// toList() 实现 (简化):
Collector.of(
ArrayList::new, // supplier: 创建 ArrayList
ArrayList::add, // accumulator: 逐个添加
(left, right) -> { // combiner: 并行合并
left.addAll(right);
return left;
},
Collector.Characteristics.IDENTITY_FINISH // 无需 finisher
);
collect 串行执行:
supplier() → 创建容器 (ArrayList)
accumulator() → 逐元素 add
finisher() → 返回容器
collect 并行执行:
多个线程各创建容器
各自累加元素
combiner() 合并多个容器
finisher() 返回最终结果
Thread1: [1,3,5] ─┐
Thread2: [2,4,6] ─┤── combiner ──▶ [1,3,5,2,4,6]
Thread3: [7,8,9] ─┘
七、常见面试追问
Q1:Stream 惰性求值有什么好处?
- 短路优化:findFirst 找到后不再处理后续元素
- 融合操作:多个中间操作在一次遍历中完成
- 无限流:
Stream.iterate(0, n -> n+1)可以配合 limit 使用
Q2:parallelStream 什么时候比 stream 快?
数据量大(>10000)、计算密集、无共享状态、无顺序依赖时。否则线程切换和分割的开销可能超过并行收益。
Q3:Stream 和 for 循环性能差异?
对于简单遍历,for 循环更快(无 Stream 对象创建开销)。对于复杂操作链(filter + map + sorted),Stream 的融合优化可能更快。但在大部分业务场景中差异不大。
Q4:Stream 是一次性的吗?
是的。一个 Stream 只能被消费一次(调用一次终端操作),重复使用会抛出 IllegalStateException。
java
Stream<Integer> s = Stream.of(1, 2, 3);
s.filter(n -> n > 0).count(); // OK
s.filter(n -> n > 0).count(); // IllegalStateException!
Q5:forEach 和 forEachOrdered 的区别?
forEach: 并行流下无序执行,性能更好forEachOrdered: 保证顺序执行,但并行流下失去并行优势
八、总结
Stream 核心设计:
管道模式: 中间操作链 + 终端操作
惰性求值: 终端操作触发执行,短路操作提前终止
Spliterator: 可分割迭代器,支撑并行流
Sink 链: 倒序包装,数据从 Head 流向 Terminal
并行流:
底层: ForkJoinPool.commonPool()
分割: Spliterator.trySplit()
合并: Collector.combiner()
陷阱: 共享状态、顺序依赖、小数据量、共享线程池
Stream API 的设计哲学是 声明式编程 + 惰性求值。理解管道构建、Sink 链、短路机制和并行流原理,才能写出高效且正确的 Stream 代码。