Stream API
如果说前面的集合是”仓库”,那 Stream 就是流过仓库的”流水线”——它把数据当作一条流水线上的产品,经过一道道工序(过滤、转换、排序、收集),最终产出结果。Java 8 引入的 Stream API 是集合框架的一次革命:它让你从”怎么遍历”的命令式思维,跃迁到”想要什么”的声明式思维。
这一章是第四阶段的压轴。我们看 Stream 怎么把十几行 for 循环压缩成一行优雅的链式调用。
一、为什么需要 Stream
先看一个需求:从一堆学生里找出分数大于 80 的女生,按分数降序,取前 3 名的名字。
命令式写法(传统 for 循环):
List<Student> filtered = new ArrayList<>();
for (Student s : students) {
if (s.getScore() > 80 && s.getGender() == Gender.FEMALE) {
filtered.add(s);
}
}
filtered.sort((a, b) -> b.getScore() - a.getScore());
List<String> result = new ArrayList<>();
for (int i = 0; i < Math.min(3, filtered.size()); i++) {
result.add(filtered.get(i).getName());
}
声明式写法(Stream):
List<String> result = students.stream()
.filter(s -> s.getScore() > 80)
.filter(s -> s.getGender() == Gender.FEMALE)
.sorted(Comparator.comparingInt(Student::getScore).reversed())
.limit(3)
.map(Student::getName)
.collect(Collectors.toList());
同样是 7 步逻辑,Stream 把它压缩成一条”流水线”——每一道工序一目了然,没有中间变量、没有索引、没有循环模板。这就是声明式编程的魅力:描述”做什么”,而不是”怎么做”。
二、Stream 是什么
首先要纠正一个常见误解:Stream 不是数据结构。它不存储数据,而是描述”对数据的操作序列”。
Stream 的关键特性:
- 不存储数据:它源于集合/数组/生成器,本身不存数据。
- 不修改源:操作 Stream 不会改变原集合。
- ** lazy 求值**:中间操作(filter/map 等)不会立即执行,直到终端操作触发。
- 一次性:一个 Stream 只能消费一次,用完即弃。
- 可能无限:Stream 可以表示无限序列(如
Stream.iterate),靠limit截断。
List<Integer> nums = List.of(1, 2, 3, 4, 5);
Stream<Integer> s = nums.stream(); // 创建流
// nums 没变;s 也没有"装"任何东西,只是描述了"对 nums 的操作"
三、创建 Stream
3.1 从集合
List<Integer> list = List.of(1, 2, 3);
Stream<Integer> s1 = list.stream(); // 顺序流
Stream<Integer> s2 = list.parallelStream(); // 并行流
3.2 从数组
String[] arr = {"A", "B", "C"};
Stream<String> s = Arrays.stream(arr);
3.3 静态工厂方法
Stream<Integer> s1 = Stream.of(1, 2, 3);
Stream<Integer> s2 = Stream.ofNullable(null); // Java 9+,0 或 1 个元素
Stream<Double> s3 = Stream.generate(Math::random); // 无限流
Stream<Integer> s4 = Stream.iterate(1, n -> n + 1); // 无限流 1,2,3,...
// Java 9+ iterate 带终止条件
Stream<Integer> s5 = Stream.iterate(1, n -> n <= 100, n -> n + 1); // 1..100
3.4 其他来源
// 字符串字符流
IntStream chars = "hello".chars(); // 104, 101, 108, 108, 111
// 文件行
Stream<String> lines = Files.lines(Path.of("file.txt"));
// 数值流范围
IntStream.range(1, 5); // 1,2,3,4
IntStream.rangeClosed(1, 5); // 1,2,3,4,5
四、中间操作
中间操作返回 Stream,可以链式调用。它们是 lazy 的——不立即执行。
4.1 filter:过滤
Stream<Integer> s = Stream.of(1, 2, 3, 4, 5).filter(n -> n > 2); // 3,4,5
4.2 map:映射
Stream<String> s = Stream.of(1, 2, 3).map(n -> "第" + n + "名"); // 第1名,第2名,第3名
map 是一对一转换。如果转换函数本身返回 Stream,要用 flatMap 拍平。
4.3 flatMap:扁平化
List<List<Integer>> nested = List.of(List.of(1, 2), List.of(3, 4), List.of(5));
// 用 map 会得到 Stream<Stream<Integer>>,不好用
// 用 flatMap 拍平成 Stream<Integer>
Stream<Integer> flat = nested.stream().flatMap(List::stream); // 1,2,3,4,5
flatMap 把”流中流”合并成一个流——类似”把多个抽屉里的东西全倒在桌上”。
经典应用:分词后扁平化。
List<String> sentences = List.of("hello world", "java stream");
List<String> words = sentences.stream()
.flatMap(s -> Arrays.stream(s.split(" ")))
.collect(Collectors.toList()); // [hello, world, java, stream]
4.4 sorted:排序
Stream<Integer> s = Stream.of(3, 1, 2).sorted(); // 1,2,3
Stream<Integer> s2 = Stream.of(3, 1, 2).sorted(Comparator.reverseOrder()); // 3,2,1
4.5 distinct:去重
Stream<Integer> s = Stream.of(1, 2, 2, 3, 3, 3).distinct(); // 1,2,3
distinct 用 equals 判断相等,相当于”流版 HashSet”。
4.6 peek:偷看
Stream<Integer> s = Stream.of(1, 2, 3)
.peek(n -> System.out.println("处理: " + n)); // 调试用,不改变元素
peek 主要用于调试——在流水线中间插入一个”窥视”动作,看元素流过的状态。
4.7 limit / skip
Stream<Integer> s = Stream.of(1, 2, 3, 4, 5).limit(3); // 1,2,3(取前 3)
Stream<Integer> s2 = Stream.of(1, 2, 3, 4, 5).skip(2); // 3,4,5(跳过前 2)
Stream<Integer> s3 = Stream.of(1, 2, 3, 4, 5).skip(1).limit(2); // 2,3
limit 在无限流上特别有用——Stream.iterate(1, n -> n+1).limit(10) 取前 10 个。
4.8 takeWhile / dropWhile(Java 9+)
Stream<Integer> s1 = Stream.of(1, 2, 3, 4, 5).takeWhile(n -> n < 4); // 1,2,3
Stream<Integer> s2 = Stream.of(1, 2, 3, 4, 5).dropWhile(n -> n < 4); // 4,5
takeWhile 在条件为 false 时停止;dropWhile 在条件为 false 时开始。它们和 filter 不同——filter 会遍历所有元素,而 takeWhile/dropWhile 是”遇到第一个不满足就停”。
五、终端操作
终端操作触发流水线执行,产生最终结果。没有终端操作,中间操作不会执行。
5.1 forEach
Stream.of(1, 2, 3).forEach(System.out::println);
5.2 collect:收集
最强大的终端操作,把流元素收集成各种结果:
List<Integer> list = stream.collect(Collectors.toList());
Set<Integer> set = stream.collect(Collectors.toSet());
Map<String, Integer> map = stream.collect(Collectors.toMap(Object::toString, n -> n));
String joined = Stream.of("A", "B", "C").collect(Collectors.joining(", ", "[", "]")); // [A, B, C]
详见下文 Collectors 一节。
5.3 reduce:归约
把流元素反复合并成一个值:
// 求和
int sum = Stream.of(1, 2, 3, 4, 5).reduce(0, Integer::sum); // 15
// 求积
int product = Stream.of(1, 2, 3, 4).reduce(1, (a, b) -> a * b); // 24
// 无初始值,返回 Optional
Optional<Integer> max = Stream.of(3, 1, 4, 1, 5).reduce(Integer::max); // Optional[5]
reduce(identity, accumulator) 的过程:identity op e1 op e2 op e3 ...。identity 是初始值(满足 identity op x == x 的”幺元”)。
5.4 count / min / max
long count = Stream.of(1, 2, 3).count(); // 3
Optional<Integer> min = Stream.of(3, 1, 2).min(Comparator.naturalOrder()); // Optional[1]
Optional<Integer> max = Stream.of(3, 1, 2).max(Comparator.naturalOrder()); // Optional[3]
min/max 返回 Optional——空流时返回 Optional.empty()。
5.5 匹配:anyMatch / allMatch / noneMatch
boolean hasEven = Stream.of(1, 2, 3).anyMatch(n -> n % 2 == 0); // true
boolean allEven = Stream.of(2, 4, 6).allMatch(n -> n % 2 == 0); // true
boolean noEven = Stream.of(1, 3, 5).noneMatch(n -> n % 2 == 0); // true
它们都是短路的——anyMatch 找到一个 true 就停,allMatch 找到一个 false 就停。
5.6 查找:findFirst / findAny
Optional<Integer> first = Stream.of(1, 2, 3).findFirst(); // Optional[1]
Optional<Integer> any = Stream.of(1, 2, 3).findAny(); // 任意一个(并行流更快)
六、Collectors:收集器大全
Collectors 是 collect 的”配方库”,提供各种收集方式。
6.1 toList / toSet / toCollection
stream.collect(Collectors.toList()); // 转 List
stream.collect(Collectors.toSet()); // 转 Set
stream.collect(Collectors.toCollection(LinkedList::new)); // 指定集合类型
💡 注意
Collectors.toList()返回的是ArrayList,但不能保证未来版本不变。要确保类型用toCollection(ArrayList::new)。
6.2 toMap
Map<String, Integer> map = students.stream()
.collect(Collectors.toMap(Student::getName, Student::getScore));
坑:如果有重复 key,会抛 IllegalStateException。要解决,传第三个参数(合并函数):
Map<String, Integer> map = students.stream()
.collect(Collectors.toMap(
Student::getName,
Student::getScore,
(oldV, newV) -> newV // 重复时取新值
));
6.3 groupingBy:分组
按某个属性分组,结果是 Map<键, List<元素>>:
Map<Gender, List<Student>> byGender = students.stream()
.collect(Collectors.groupingBy(Student::getGender));
// {FEMALE=[...], MALE=[...]}
二级分组:
Map<Gender, Map<Integer, List<Student>>> byGenderAndScore = students.stream()
.collect(Collectors.groupingBy(Student::getGender,
Collectors.groupingBy(s -> s.getScore() / 10)));
分组后做下游收集(不只是 toList):
Map<Gender, Long> countByGender = students.stream()
.collect(Collectors.groupingBy(Student::getGender, Collectors.counting()));
Map<Gender, Double> avgScoreByGender = students.stream()
.collect(Collectors.groupingBy(Student::getGender,
Collectors.averagingInt(Student::getScore)));
Map<Gender, Optional<Student>> topByGender = students.stream()
.collect(Collectors.groupingBy(Student::getGender,
Collectors.maxBy(Comparator.comparingInt(Student::getScore))));
6.4 partitioningBy:分区
按布尔条件分成两组(true 一组,false 一组):
Map<Boolean, List<Student>> byPass = students.stream()
.collect(Collectors.partitioningBy(s -> s.getScore() >= 60));
// {true=[及格的...], false=[不及格的...]}
6.5 joining:字符串拼接
String s1 = Stream.of("A", "B", "C").collect(Collectors.joining()); // ABC
String s2 = Stream.of("A", "B", "C").collect(Collectors.joining(", ")); // A, B, C
String s3 = Stream.of("A", "B", "C").collect(Collectors.joining(", ", "[", "]")); // [A, B, C]
6.6 counting / summing / averaging / summarizing
long count = stream.collect(Collectors.counting());
int sum = stream.collect(Collectors.summingInt(Student::getScore));
double avg = stream.collect(Collectors.averagingInt(Student::getScore));
IntSummaryStatistics stats = stream.collect(Collectors.summarizingInt(Student::getScore));
// stats.getCount(), getSum(), getMin(), getAverage(), getMax()
summarizingInt 一次返回 count/sum/min/avg/max 全部统计——超方便。
七、数值流:IntStream / LongStream / DoubleStream
普通 Stream<Integer> 装箱开销大。处理数值时,用原始类型特化的 IntStream 等:
int sum = IntStream.of(1, 2, 3, 4, 5).sum(); // 15,无装箱
// 从普通流转数值流
int sum2 = students.stream().mapToInt(Student::getScore).sum();
// 范围
IntStream.range(1, 5).forEach(System.out::println); // 1,2,3,4
IntStream.rangeClosed(1, 5).forEach(System.out::println); // 1,2,3,4,5
// 统计
IntSummaryStatistics stats = IntStream.of(3, 1, 4, 1, 5).summaryStatistics();
System.out.println(stats.getAverage()); // 2.8
System.out.println(stats.getMax()); // 5
mapToInt / mapToLong / mapToDouble 把对象流变成数值流;boxed() 把数值流变回对象流。
八、并行流
.parallel() 或 parallelStream() 让流并行执行——内部用 ForkJoinPool 自动切分任务。
long count = list.parallelStream()
.filter(n -> isPrime(n))
.count();
并行流的适用条件:
- 数据量大(至少 1 万以上,否则切分开销大于收益)。
- 每个元素计算重(计算简单的话,并行收益不明显)。
- 操作无状态、无副作用(操作之间不依赖顺序)。
- 元素顺序无关紧要(或用
forEachOrdered保序)。
注意事项:
- 并行流不保证顺序——
forEach的输出顺序可能乱。 - 共享可变状态是灾难——
forEach里修改共享变量会数据竞争。 - 终端操作
collect会用合并函数合并各子任务结果——要确保合并满足结合律。
// ❌ 并行流 + 共享可变状态 = 灾难
ArrayList<Integer> result = new ArrayList<>();
list.parallelStream().forEach(result::add); // 数据丢失、null、IndexOutOfBoundsException
// ✅ 用线程安全收集器
List<Integer> result = list.parallelStream().collect(Collectors.toList());
九、实战:用 Stream 处理学生成绩
这个例子把 Stream API 的精华一网打尽:filter、map、sorted、limit、collect、groupingBy、partitioningBy、joining、summarizingInt、reduce、mapToInt、rangeClosed——日常 90% 的 Stream 操作都在这里了。
十、Stream 的设计哲学
Stream 的精妙在于它把”数据处理”抽象成了三个阶段:
源 → [中间操作] → 终端操作
中间操作是 lazy 的——它们不立即执行,而是”记下”要做的事。直到终端操作触发,整条流水线才一次性执行,每个元素”流过”所有工序。
这种设计有几个好处:
- 可优化:流水线知道全部操作,可以做”循环融合”——多个操作在一次遍历中完成,而不是多次遍历。
- 短路:
limit、findFirst、anyMatch等不需要遍历全部元素。 - 无限流:因为 lazy,
Stream.iterate可以表示无限序列,靠limit截断。
十一、本章小结
| 主题 | 要点 |
|---|---|
| Stream 本质 | 不是数据结构,是操作序列;lazy、不修改源、一次性 |
| 创建 | Collection.stream()、Stream.of、generate、iterate、Arrays.stream |
| 中间操作 | filter/map/flatMap/sorted/distinct/peek/limit/skip/takeWhile |
| 终端操作 | forEach/collect/reduce/count/min/max/anyMatch/findFirst |
| Collectors | toList/toMap/groupingBy/partitioningBy/joining/summarizingInt |
| 数值流 | IntStream/LongStream,避免装箱 |
| reduce | 归约成单个值,需要幺元和结合函数 |
| 并行流 | .parallel(),大数据+重计算+无状态才用 |
| 并行流陷阱 | 不能修改共享可变状态 |
结语:声明式的胜利
Stream API 是 Java 走向现代语言的关键一步。它把数据处理从”循环+临时变量+索引”的命令式泥潭,提升到了”过滤-映射-收集”的声明式优雅。
但 Stream 不是万能的:
- 简单遍历用 for-each 更直观,不必强行 Stream。
- 修改元素的场景,Stream 不如 for 循环方便(Stream 倡导无副作用)。
- 性能敏感的小数据,Stream 的开销可能比 for 循环大。
掌握 Stream 的关键是:用”流水线”思维想问题——数据从源头流出,经过一道道工序,最终汇入结果容器。每道工序都是简洁的函数,组合起来却表达出强大的逻辑。
至此,第四阶段”集合框架”全部完成。我们走过了:体系总览、List、Set、Map、Queue/Deque、Collections 工具、Stream API。这套集合框架是 Java 程序员的核心武器库,几乎每个程序都离不开它们。
下一阶段,我们将进入 函数式编程——Lambda、函数式接口、方法引用。它们是 Stream API 的基石,也是 Java 现代编程风格的灵魂。