跳转至

Stream API


1. 引入:它解决了什么问题?

Stream API 让集合操作(过滤、转换、聚合)从命令式循环变为声明式链式调用,代码更简洁、可读性更强。

// 传统写法:找出所有长度>3的名字,转大写,排序
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Di");
List<String> result = new ArrayList<>();
for (String name : names) {
    if (name.length() > 3) {
        result.add(name.toUpperCase());
    }
}
Collections.sort(result);

// Stream 写法(链式,一目了然)
List<String> result = names.stream()
    .filter(name -> name.length() > 3)
    .map(String::toUpperCase)
    .sorted()
    .collect(Collectors.toList());

2. Stream 操作分类

flowchart LR
    A["数据源\nCollection/Array"] --> B["中间操作\n惰性求值"]
    B --> C["终止操作\n触发执行"]

    subgraph 中间操作["中间操作 — 返回 Stream,不立即执行"]
        D[filter 过滤]
        E[map 转换]
        F[flatMap 扁平化]
        G[sorted 排序]
        H[distinct 去重]
        I[limit/skip 截取]
        J[peek 调试]
    end

    subgraph 终止操作["终止操作 — 返回结果,触发执行"]
        K[collect 收集]
        L[forEach 遍历]
        M[count 计数]
        N[findFirst/findAny]
        O[anyMatch/allMatch]
        P[reduce 聚合]
    end

关键原理:惰性求值 中间操作不会立即执行,只有遇到终止操作时才会触发整个流水线的执行。 为什么这样设计:惰性求值允许短路优化——如 findFirst() 找到第一个就停止,不需要处理所有元素;limit(10) 只处理前 10 个元素,后面的中间操作根本不执行。


3. 常用操作示例

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// filter + collect:过滤偶数
List<Integer> evens = numbers.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList()); // [2, 4, 6, 8, 10]

// map:平方
List<Integer> squares = numbers.stream()
    .map(n -> n * n)
    .collect(Collectors.toList()); // [1, 4, 9, 16, ...]

// reduce:求和
int sum = numbers.stream()
    .reduce(0, Integer::sum); // 55

// groupingBy:按奇偶分组
Map<Boolean, List<Integer>> grouped = numbers.stream()
    .collect(Collectors.groupingBy(n -> n % 2 == 0));
// {false=[1,3,5,7,9], true=[2,4,6,8,10]}

// flatMap:扁平化嵌套列表
List<List<Integer>> nested = Arrays.asList(
    Arrays.asList(1, 2), Arrays.asList(3, 4));
List<Integer> flat = nested.stream()
    .flatMap(Collection::stream)
    .collect(Collectors.toList()); // [1, 2, 3, 4]

4. 综合实战

需求:从用户列表中找出年龄大于18岁的活跃用户,取其邮箱,去重后按字母排序。

// 函数式写法:Stream + Lambda + Optional 组合
List<String> getActiveUserEmails(List<User> users) {
    return users.stream()
        .filter(u -> u.getAge() > 18 && u.isActive())  // 过滤条件
        .map(User::getEmail)                             // 提取邮箱
        .filter(Objects::nonNull)                        // 过滤空值
        .distinct()                                      // 去重
        .sorted()                                        // 排序
        .collect(Collectors.toList());                   // 收集结果
}

5. 工作中常见坑

坑点 问题描述 根本原因 解决方案
Stream 只能消费一次 对同一个 Stream 调用两次终止操作会抛异常 Stream 是一次性的流水线,消费后状态变为"已关闭" 每次从数据源重新创建 Stream
并行流线程安全 parallelStream() 操作非线程安全集合会出错 并行流在 ForkJoinPool 中多线程执行,共享状态会竞争 使用 collect() 而非直接 add()
空指针异常 map() 返回 null 后续操作 NPE Stream 不会自动处理 null 值 使用 filter(Objects::nonNull) 或 Optional
性能误区 小数据量用 Stream 反而更慢 Stream 有创建流水线的开销,数据量小时开销占比大 数据量小时用普通 for 循环

坑点代码示例

// ❌ 坑1:Stream 只能消费一次
Stream<String> stream = list.stream().filter(s -> s.length() > 3);
long count = stream.count();          // 第一次消费,OK
List<String> result = stream.collect(Collectors.toList()); // 抛 IllegalStateException!
// ✅ 每次重新创建
long count = list.stream().filter(s -> s.length() > 3).count();
List<String> result = list.stream().filter(s -> s.length() > 3).collect(Collectors.toList());

// ❌ 坑2:parallelStream 操作非线程安全集合
List<Integer> result = new ArrayList<>();
IntStream.range(0, 1000).parallel().forEach(result::add); // 数据丢失!
// ✅ 用 collect 收集,线程安全
List<Integer> result = IntStream.range(0, 1000).parallel()
    .boxed()
    .collect(Collectors.toList());

// ❌ 坑3:map 返回 null 导致后续 NPE
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<Integer> lengths = names.stream()
    .map(name -> name.equals("Bob") ? null : name.length()) // Bob 返回 null
    .collect(Collectors.toList()); // 不报错,但 list 中有 null
// 后续调用 lengths.stream().mapToInt(Integer::intValue) 会 NPE!
// ✅ 过滤 null
List<Integer> lengths = names.stream()
    .map(name -> name.equals("Bob") ? null : name.length())
    .filter(Objects::nonNull)
    .collect(Collectors.toList());

// ❌ 坑4:在 Stream 中修改外部集合(ConcurrentModificationException)
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
list.stream().forEach(s -> {
    if (s.equals("b")) list.remove(s); // 抛 ConcurrentModificationException!
});
// ✅ 用 removeIf
list.removeIf(s -> s.equals("b"));

// ❌ 坑5:peek 用于调试可以,但不要用于修改状态(有副作用)
// peek 是中间操作,在某些短路场景下可能不执行
list.stream()
    .peek(s -> System.out.println("处理: " + s)) // 调试用,OK
    .filter(s -> s.length() > 1)
    .findFirst(); // 找到第一个就停止,后面的 peek 不会执行

6. 面试高频问题

Q:Stream 的惰性求值是什么意思?

中间操作(filter/map/sorted)不会立即执行,它们只是构建了一个操作流水线。只有当终止操作(collect/forEach/count)被调用时,整个流水线才会被触发执行。好处是可以进行短路优化(如 findFirst() 找到第一个就停止)。

Q:Stream 和 for 循环哪个性能更好?

数据量大时 Stream(尤其是 parallelStream)有优势;数据量小时 for 循环更快,因为 Stream 有创建流水线的额外开销。

Q:parallelStream 一定比 stream 快吗?

不一定。parallelStream 使用 ForkJoinPool 多线程执行,适合 CPU 密集型、数据量大的场景。如果任务本身很轻量或数据量小,线程切换的开销反而会使性能更差。