Skip to content
第 37 / 250 章后端⏱ 10 分钟阅读

第 37 章:Stream API

学习目标

  • 掌握 Stream 的三段式结构
  • 熟练使用常用中间操作与终端操作
  • 掌握 Collectors 分组、统计

一、Stream 三段式

java
List<String> result = users.stream()          // ① 数据源
        .filter(u -> u.getAge() >= 18)        // ② 中间操作(惰性)
        .map(User::getName)                   // ② 中间操作(惰性)
        .toList();                            // ③ 终端操作(触发执行)

惰性求值:中间操作只是「记录要做什么」,不执行。直到遇到终端操作才一次性跑完。没有终端操作,整条链一行代码都不会执行。

二、创建 Stream

java
// 从集合
list.stream();
list.parallelStream();              // 并行流

// 从数组
Arrays.stream(new int[]{1, 2, 3});

// 直接创建
Stream.of("a", "b", "c");

// 生成无限流(必须配 limit)
Stream.iterate(1, n -> n * 2).limit(10);        // 1,2,4,8,...
Stream.generate(Math::random).limit(5);

// 数值流(避免装箱,性能更好)
IntStream.range(0, 10);          // 0..9
IntStream.rangeClosed(1, 10);    // 1..10

// 从文件
Files.lines(Path.of("a.txt"));

三、中间操作

操作作用
filter(Predicate)过滤
map(Function)一对一转换
flatMap(Function)一对多展平
distinct()去重(依赖 equals)
sorted() / sorted(Comparator)排序
limit(n)取前 n 个
skip(n)跳过前 n 个
peek(Consumer)查看元素(调试用)
java
List<String> words = List.of("apple", "banana", "cherry", "apple");

words.stream()
     .filter(w -> w.length() > 5)      // banana, cherry
     .distinct()
     .sorted()
     .limit(2)
     .forEach(System.out::println);

map vs flatMap

java
// map:一对一
List<String> names = users.stream().map(User::getName).toList();

// flatMap:一对多展平
List<List<String>> nested = List.of(
        List.of("a", "b"),
        List.of("c", "d")
);

List<String> flat = nested.stream()
        .flatMap(List::stream)        // ① 把每个 List 变成 Stream 再合并
        .toList();                    // [a, b, c, d]

实战场景

java
// 取出所有订单里的所有商品名
List<String> allProducts = orders.stream()
        .flatMap(o -> o.getItems().stream())
        .map(Item::getProductName)
        .distinct()
        .toList();

四、终端操作

java
// 遍历
stream.forEach(System.out::println);

// 收集
List<T> list = stream.toList();                    // JDK 16+,不可变
List<T> list2 = stream.collect(Collectors.toList());
Set<T> set = stream.collect(Collectors.toSet());

// 统计
long count = stream.count();
Optional<T> max = stream.max(Comparator.comparing(User::getAge));
Optional<T> min = stream.min(...);

// 匹配(短路操作,找到就停)
boolean any = stream.anyMatch(u -> u.getAge() > 60);
boolean all = stream.allMatch(u -> u.getAge() > 0);
boolean none = stream.noneMatch(u -> u.getAge() < 0);

// 查找
Optional<T> first = stream.findFirst();
Optional<T> any2 = stream.findAny();     // 并行流下更快

// 归约
int sum = stream.reduce(0, Integer::sum);

reduce 详解

java
List<Integer> nums = List.of(1, 2, 3, 4, 5);

// ① 无初始值:返回 Optional
Optional<Integer> sum1 = nums.stream().reduce((a, b) -> a + b);

// ② 有初始值:返回具体类型
int sum2 = nums.stream().reduce(0, (a, b) -> a + b);      // 15

// ③ 求最大值
int max = nums.stream().reduce(Integer.MIN_VALUE, Integer::max);

执行过程((((0+1)+2)+3)+4)+5 — 把两两合并的逻辑重复应用到整个序列。

五、Collectors 高级收集

java
List<User> users = getUsers();

// ① 转 Map(注意 key 重复会抛异常)
Map<Long, User> map = users.stream()
        .collect(Collectors.toMap(User::getId, u -> u));

// ② key 重复时的合并策略
Map<String, User> byName = users.stream()
        .collect(Collectors.toMap(
                User::getName,
                u -> u,
                (existing, replacement) -> existing));   // 保留先出现的

// ③ 分组
Map<String, List<User>> byCity = users.stream()
        .collect(Collectors.groupingBy(User::getCity));

// ④ 分组 + 统计
Map<String, Long> countByCity = users.stream()
        .collect(Collectors.groupingBy(User::getCity, Collectors.counting()));

// ⑤ 分组 + 取字段列表
Map<String, List<String>> namesByCity = users.stream()
        .collect(Collectors.groupingBy(
                User::getCity,
                Collectors.mapping(User::getName, Collectors.toList())));

// ⑥ 二级分组
Map<String, Map<Integer, List<User>>> nested = users.stream()
        .collect(Collectors.groupingBy(User::getCity,
                 Collectors.groupingBy(User::getAge)));

// ⑦ 分区(按 boolean 分两组)
Map<Boolean, List<User>> adults = users.stream()
        .collect(Collectors.partitioningBy(u -> u.getAge() >= 18));

// ⑧ 拼接字符串
String names = users.stream()
        .map(User::getName)
        .collect(Collectors.joining(", ", "[", "]"));   // [张三, 李四]

// ⑨ 数值统计一次搞定
IntSummaryStatistics stats = users.stream()
        .collect(Collectors.summarizingInt(User::getAge));
System.out.println(stats.getMax() + " " + stats.getAverage());

⚠️ toMap 最常见的坑:value 为 null 时抛 NPE;key 重复时抛 IllegalStateException。业务数据不可控时一定要传第三个参数

六、排序技巧

java
// 单字段
users.stream().sorted(Comparator.comparing(User::getAge));

// 倒序
users.stream().sorted(Comparator.comparing(User::getAge).reversed());

// 多字段:先按城市,再按年龄倒序
users.stream().sorted(
        Comparator.comparing(User::getCity)
                  .thenComparing(User::getAge, Comparator.reverseOrder()));

// null 值排最后
users.stream().sorted(
        Comparator.comparing(User::getName,
                Comparator.nullsLast(Comparator.naturalOrder())));

七、并行流

java
// 顺序流
list.stream().filter(...).count();

// 并行流:底层用 ForkJoinPool.commonPool()
list.parallelStream().filter(...).count();

什么时候用?

适合不适合
数据量大(> 1 万)数据量小(并行开销 > 收益)
每个元素计算耗时简单操作(如求和)
无状态、无共享变量涉及 IO、数据库
数据源易拆分(ArrayList)LinkedList(拆分成本高)
java
// ❌ 并行流 + 共享可变状态 = 数据错乱
List<Integer> result = new ArrayList<>();      // ArrayList 非线程安全
list.parallelStream().forEach(result::add);    // ❌ 可能丢数据/抛异常

// ✅ 用 collect,框架保证线程安全
List<Integer> result = list.parallelStream().collect(Collectors.toList());

⚠️ 生产警告:并行流默认共用一个全局 ForkJoinPool。一个慢任务会阻塞整个应用的所有并行流。Web 应用中慎用,需要并行请用自己的线程池。

八、常见坑

java
// 坑 1:Stream 只能用一次
Stream<String> s = list.stream();
s.forEach(...);
s.count();       // ❌ IllegalStateException: stream has already been operated upon

// 坑 2:peek 在没有终端操作时不执行
list.stream().peek(System.out::println);    // ❌ 什么都不打印

// 坑 3:forEach 中修改源集合
list.stream().forEach(x -> list.remove(x));  // ❌ ConcurrentModificationException

// 坑 4:Optional.get() 不判空
users.stream().findFirst().get();            // ❌ 空时 NoSuchElementException
users.stream().findFirst().orElse(null);     // ✅
users.stream().findFirst().orElseThrow(() -> new BusinessException(...));  // ✅

九、本章小结

要点关键
三段式数据源 → 中间操作 → 终端操作
惰性没有终端操作就不执行
map / flatMap一对一 / 一对多展平
groupingBy分组,可嵌套可组合
toMap必须处理 key 冲突
一次性Stream 用完即废
并行流Web 应用慎用

动手练习

练习 1:基础题

给定 List<Employee>(含 name、department、salary),用 Stream 求:

  1. 每个部门的平均薪资
  2. 薪资最高的员工
  3. 所有部门名称(去重,逗号拼接)

练习 2:进阶题

实现「统计一篇文章中出现频率 Top 10 的单词」,要求过滤掉长度小于 3 的词,忽略大小写。


下一章第 38 章:多线程基础

本站基于 VitePress 构建 · 由 Codebook 团队维护