这是「Java 从基础到实战」系列的第 5 篇。Stream 是 Java 8 之后处理集合的标准姿势,本文不讲理论,直接给 20 个日常开发最高频的可复制片段:过滤、映射、分组、归约、排序一网打尽。所有代码在 JDK 17 下可直接运行。
前言
同样一个「筛选 + 转换 + 统计」的需求,for 循环要写 10 行,Stream 一行搞定且可读性更好。本文所有例子基于同一份学生数据,边看边在 IDEA 里跑。
环境准备
| 软件 |
版本 |
| JDK |
17(Eclipse Temurin) |
| IntelliJ IDEA |
Community 社区版 |
在 java-basics 项目中新建包 stream,创建统一的测试数据类 Data.java:
package stream;
import java.util.List;
public class Data { public record Student(String name, String clazz, int score) {}
public static final List<Student> STUDENTS = List.of( new Student("张三", "一班", 85), new Student("李四", "一班", 58), new Student("王五", "二班", 92), new Student("赵六", "二班", 74), new Student("孙七", "三班", 92), new Student("周八", "三班", 45) ); }
|
Stream 的固定套路:数据源 → 中间操作(可多个,惰性)→ 终止操作(触发执行)。
例 1~5:过滤与查找
新建 FilterDemo.java(后续例子可都写在各自 main 中运行验证):
List<Student> passed = STUDENTS.stream() .filter(s -> s.score() >= 60) .toList();
List<Student> r2 = STUDENTS.stream() .filter(s -> s.clazz().equals("一班")) .filter(s -> s.score() >= 60) .toList();
boolean hasExcellent = STUDENTS.stream().anyMatch(s -> s.score() >= 90);
boolean allPassed = STUDENTS.stream().allMatch(s -> s.score() >= 60);
Optional<Student> firstFailed = STUDENTS.stream() .filter(s -> s.score() < 60) .findFirst(); firstFailed.ifPresent(s -> System.out.println("第一个不及格: " + s.name()));
|
例 6~10:映射与转换
List<String> names = STUDENTS.stream().map(Student::name).toList();
List<Integer> adjusted = STUDENTS.stream().map(s -> s.score() + 5).toList();
List<Integer> distinctScores = STUDENTS.stream() .map(Student::score).distinct().toList();
List<List<String>> classes = List.of(List.of("张三", "李四"), List.of("王五")); List<String> flat = classes.stream().flatMap(List::stream).toList();
IntSummaryStatistics stats = STUDENTS.stream() .mapToInt(Student::score).summaryStatistics(); System.out.printf("平均 %.1f,最高 %d,最低 %d%n", stats.getAverage(), stats.getMax(), stats.getMin());
|
例 11~15:排序与截取
List<Student> asc = STUDENTS.stream() .sorted(Comparator.comparingInt(Student::score)).toList();
List<Student> desc = STUDENTS.stream() .sorted(Comparator.comparingInt(Student::score).reversed() .thenComparing(Student::name)) .toList();
List<Student> top3 = STUDENTS.stream() .sorted(Comparator.comparingInt(Student::score).reversed()) .limit(3).toList();
List<Student> rest = STUDENTS.stream() .sorted(Comparator.comparingInt(Student::score).reversed()) .skip(3).toList();
Optional<Student> topOne = STUDENTS.stream() .max(Comparator.comparingInt(Student::score));
|
例 16~20:收集与分组(collect 是精华)
Map<String, Integer> nameScore = STUDENTS.stream() .collect(Collectors.toMap(Student::name, Student::score));
Map<String, List<Student>> byClass = STUDENTS.stream() .collect(Collectors.groupingBy(Student::clazz));
Map<String, Long> countByClass = STUDENTS.stream() .collect(Collectors.groupingBy(Student::clazz, Collectors.counting()));
Map<String, Double> avgByClass = STUDENTS.stream() .collect(Collectors.groupingBy(Student::clazz, Collectors.averagingInt(Student::score)));
Map<Boolean, List<Student>> passFail = STUDENTS.stream() .collect(Collectors.partitioningBy(s -> s.score() >= 60)); System.out.println("不及格名单: " + passFail.get(false).stream() .map(Student::name).collect(Collectors.joining("、")));
|
常见坑
坑 1:Stream 只能消费一次
错误示范 ❌:
Stream<Student> s = STUDENTS.stream(); s.count(); s.toList();
|
正确写法:每次操作都从集合重新 .stream() ✅。
坑 2:toMap 遇到重复 key 直接抛异常
错误示范 ❌:
Map<Integer, String> m = STUDENTS.stream() .collect(Collectors.toMap(Student::score, Student::name));
|
正确写法:提供第三个参数(合并函数)处理冲突 ✅:
Map<Integer, String> m = STUDENTS.stream() .collect(Collectors.toMap(Student::score, Student::name, (a, b) -> a + "," + b));
|
坑 3:滥用 parallelStream
错误示范:数据量几百条也上并行流 ❌。并行流有线程调度开销,小数据量反而更慢,且默认共用全局 ForkJoinPool,在 Web 应用里可能互相拖累。
正确写法:默认用串行流;只有「数据量大(万级以上)+ 无共享状态 + CPU 密集」才考虑并行,并用实际压测验证 ✅。
小结
- ✅ 套路:数据源 → 中间操作(filter/map/sorted)→ 终止操作(toList/collect/count)
- ✅ 最高频组合:
filter + map + toList、groupingBy + counting/averagingInt
- ✅ 三个坑:流不可复用、toMap 重复 key、并行流别乱用
下一篇是阶段一收官:《Java IO 与 NIO 入门:文件读写的 5 种方式及性能对比》,附实测数据,敬请期待。
上一篇:《Java 异常处理最佳实践:try-with-resources 与自定义异常》
本系列完整目录见博客「技术博文」分类。
💬 评论