第 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 求:
- 每个部门的平均薪资
- 薪资最高的员工
- 所有部门名称(去重,逗号拼接)
练习 2:进阶题
实现「统计一篇文章中出现频率 Top 10 的单词」,要求过滤掉长度小于 3 的词,忽略大小写。
下一章:第 38 章:多线程基础 →