Skip to content

07.2 — Stream 流水线

定位: 从"怎么做"到"做什么" — 声明式数据处理的核心机制 面试高频度: ⭐⭐⭐⭐ 考查方式: 中间操作 vs 终端操作、惰性求值、并行流原理

一、这是什么?为什么需要它?

从外部迭代到内部迭代

java
// 传统:外部迭代(告诉计算机怎么做)
List<String> result = new ArrayList<>();
for (String s : list) {
    if (s.startsWith("A")) {
        result.add(s.toUpperCase());
    }
}

// Stream:内部迭代(告诉计算机要什么)
List<String> result = list.stream()
    .filter(s -> s.startsWith("A"))
    .map(String::toUpperCase)
    .collect(Collectors.toList());

为什么需要 Stream?

1. 声明式 — 聚焦"要什么",而非"怎么做"
2. 可组合 — filter → map → collect 像流水线
3. 惰性求值 — 只在最后一次性计算
4. 并行友好 — parallelStream() 一键并行
5. 避免副作用 — 不修改源数据

二、原理拆解

2.1 Stream 的三阶段模型

┌─────────────────────────────────────────────────────────────┐
│              Stream 操作三阶段                                │
│                                                              │
│  数据源 ──→ 中间操作(1..N) ──→ 终端操作                       │
│  (集合/      filter       collect                            │
│   数组/      map          forEach                            │
│   文件)      sorted       reduce                             │
│              flatMap      count                              │
│              distinct      ...                               │
│              limit                                            │
│              skip                                             │
│                                                              │
│  中间操作:惰性(lazy),构建操作流水线,不执行                 │
│  终端操作:触发执行(eager),消费 Stream,完后 Stream 关闭    │
│                                                              │
│  关键:没有终端操作,中间操作不会执行!                         │
└─────────────────────────────────────────────────────────────┘

2.2 惰性求值

java
Stream<String> stream = list.stream()
    .filter(s -> {
        System.out.println("filter: " + s);
        return s.length() > 3;
    })
    .map(s -> {
        System.out.println("map: " + s);
        return s.toUpperCase();
    });

// 到目前为止,没有输出!—— 中间操作是惰性的

List<String> result = stream.collect(Collectors.toList());
// 终端操作触发 → 现在才执行!

惰性求值的意义:

不需要将所有中间结果保存在内存中。

例:从 1000 万条数据中找到前 10 个匹配项
  list.stream()
      .filter(expensivePredicate)
      .limit(10)
      .collect(toList());

如果没有惰性求值:
  filter → 计算 1000 万次 → 生成 1000 万中间结果
  limit → 取前 10 个 → 剩余 9999990 个丢弃!

有惰性求值:
  filter 和 limit 融合 → filter 找到 10 个就停止
  → 只计算约 10+ 次

⚡ 短路的威力:limit、findFirst、anyMatch 等短路操作

2.3 中间操作分类

无状态操作(每个元素独立处理):
  filter()      — 过滤
  map()         — 转换
  flatMap()     — 展平
  peek()        — 查看(调试用)
  forEach()     — 遍历(终端操作)

有状态操作(需要记录状态):
  distinct()    — 去重(需要保存已见过的元素)
  sorted()      — 排序(需要处理全部元素)
  limit()       — 限制(需要计数)
  skip()        — 跳过(需要计数)

2.4 并行流原理

java
// 一键并行
list.parallelStream()
    .filter(...)
    .map(...)
    .collect(toList());

// 等价于
list.stream()
    .parallel()
    .filter(...)
    .map(...)
    .collect(toList());
并行流底层:ForkJoinPool.commonPool()

工作原理:
  ① 分割:将数据源分成多个段(Spliterator)
  ② 处理:每个段分配给一个线程并行处理
  ③ 合并:将各段结果合并

适合并行:
  - 数据量大(>10000)
  - 每个元素处理耗时长
  - 无状态操作(filter/map)
  
不适合并行:
  - 数据量小(并行开销 > 收益)
  - 有状态操作(sorted/distinct 有合并成本)
  - 顺序依赖(limit/findFirst 需要串行语义)
  - 阻塞操作(I/O — 会占用公共池线程)

三、图解全景

┌─────────────────────────────────────────────────────────────┐
│              Stream 流水线执行示意图                           │
│                                                              │
│  数据: ["Alice", "Bob", "Amy", "David", "Anna"]              │
│                                                              │
│  操作链: filter(name -> name.startsWith("A"))                 │
│          .map(String::toUpperCase)                            │
│          .limit(2)                                           │
│          .collect(toList())                                  │
│                                                              │
│  执行(垂直处理——每个元素走完全程):                          │
│                                                              │
│  "Alice" → filter(A) ✅ → map → "ALICE" → limit(1) → 收集    │
│  "Bob"   → filter(B) ❌ → (跳过)                              │
│  "Amy"   → filter(A) ✅ → map → "AMY" → limit(2) → 收集      │
│  "David" → filter(D) ❌ → (跳过)                              │
│  "Anna"  → filter — limit 已满 → 跳过(不再处理)               │
│                                                              │
│  结果: ["ALICE", "AMY"]                                      │
│                                                              │
│  ⚡ 注意:不是先全部 filter,再全部 map!                      │
│  而是一个元素走完整个流水线再处理下一个                          │
│  这使 limit 能够提前终止                                      │
└─────────────────────────────────────────────────────────────┘

四、实战验证

4.1 惰性求值验证

java
List<String> list = Arrays.asList("Alice", "Bob", "Amy", "David");

long count = list.stream()
    .filter(s -> {
        System.out.println("filter: " + s);
        return s.length() > 3;
    })
    .count();  // 终端操作

// 输出(按元素垂直执行):
// filter: Alice
// filter: Bob
// filter: Amy
// filter: David
// 结果: count = 2

4.2 并行流性能

java
long count = LongStream.rangeClosed(1, 10_000_000)
    .filter(n -> n % 2 == 0)
    .count();

// 串行: ~50ms
// 并行: ~15ms

// 但如果是轻量操作且数据量小:
IntStream.range(1, 1000)
    .parallel()
    .map(x -> x * 2)    // 简单操作
    .sum();
// 并行反而更慢(线程创建/调度开销)

五、面试视角

追问答案要点
Stream 中间操作和终端操作的区别?中间操作惰性构建流水线;终端操作触发执行并关闭 Stream
惰性求值有什么好处?短路优化(limit)、减少中间对象、流水线融合优化
parallelStream 一定比 stream 快吗?不一定。数据量大+元素处理耗时+无状态操作才适合
parallelStream 使用的线程池?ForkJoinPool.commonPool(),默认线程数=CPU核心数-1
Stream 能复用吗?不能。终端操作后 Stream 已关闭,需重新创建
forEach 和 for-each 循环哪个快?小数据量 for-each 快;大数据量+复杂操作 Stream 快

📚 相关链接

  • **Lambda与函数式接口** — Lambda 语法基础
  • **Collectors收集器** — 收集数据的方法
  • **集合索引** — 集合作为 Stream 的数据源
  • ← 返回 **Lambda索引**

Knowledge4J — Java 知识库