精通Java Stream:如何优雅地遍历集合?7大实战案例详解
目录导读
- 为什么需要Stream遍历集合?
- Stream与传统遍历对比:谁更胜一筹?
- 7大实战案例:从基础到高阶
- 案例1:基础集合遍历(forEach)
- 案例2:条件过滤(filter)
- 案例3:数据转换(map)
- 案例4:去重与排序(distinct/sorted)
- 案例5:聚合统计(reduce/count)
- 案例6:并行流遍历(parallelStream)
- 案例7:自定义对象复杂遍历
- 常见问题与避坑指南
- Q&A:你问我答
为什么需要Stream遍历集合?
在Java 8之前,遍历集合无非就是for循环、Iterator迭代器,但面对复杂业务——比如既要过滤、又要转换、还要统计——代码会变得冗长且难以维护,Stream API正是为此而生:它让集合操作变得像SQL查询一样声明式、链式、高效。

根据Oracle官方文档和Stack Overflow的统计,合理使用Stream能使业务代码减少40%以上,且更利于并行处理。
Stream与传统遍历对比:谁更胜一筹?
| 场景 | 传统for循环 | Stream |
|---|---|---|
| 过滤+打印 | 4行+if判断 | 1行链式调用 |
| 多条件转换 | 需临时变量 | 函数式映射 |
| 大数据量 | 单线程,效率低 | 支持并行流 |
| 代码可读性 | 命令式,逻辑分散 | 声明式,意图明确 |
Stream不是替代品,而是更优雅的解决方案,尤其适合数据筛选、转换、聚合场景。
7大实战案例
以下案例均基于JDK 8+,集合类型为List
和自定义对象。
案例1:基础集合遍历(forEach)
List<String> names = Arrays.asList("Tom", "Jerry", "Alice");
// 传统方式
for (String name : names) { System.out.println(name); }
// Stream方式
names.stream().forEach(System.out::println);
图解:stream()创建流,forEach是终端操作,依次消费每个元素,不需要显式写循环变量,代码量减半。
案例2:条件过滤(filter)
需求:获取长度大于3的名字。
names.stream()
.filter(name -> name.length() > 3)
.collect(Collectors.toList());
// 结果:[Jerry, Alice]
filter接收一个Predicate(布尔返回值的函数),只有满足条件的元素才会进入下一环节,这是Stream最常用的“守门员”操作。
案例3:数据转换(map)
需求:将所有名字转为大写。
names.stream()
.map(String::toUpperCase)
.forEach(System.out::println);
// TOM JERRY ALICE
map是一对一转换,将原始元素映射成新元素,可以多次链式调用,例如先过滤再转换。
案例4:去重与排序(distinct/sorted)
List<Integer> numbers = Arrays.asList(3, 1, 4, 1, 5, 9, 2, 6, 5);
numbers.stream()
.distinct() // 去重:[3,1,4,5,9,2,6]
.sorted() // 升序排序:[1,2,3,4,5,6,9]
.collect(Collectors.toList());
注意:sorted可传入自定义Comparator,对于自定义对象,需要实现Comparable接口,或者用Lambda指定排序规则。
案例5:聚合统计(reduce/count)
统计所有数字之和:
int sum = numbers.stream().reduce(0, Integer::sum); // 31 long count = numbers.stream().filter(n -> n > 3).count(); // 4
reduce是归约操作,将流中的元素反复结合得到单个值。count是常见聚合函数。
案例6:并行流遍历(parallelStream)
当集合数据量巨大(如10万条以上),可启用并行流:
List<Transaction> bigList = ...; // 假设10万条
bigList.parallelStream()
.filter(t -> t.getAmount() > 1000)
.forEach(t -> process(t));
注意:并行流利用Fork/Join框架,自动拆分数据到多线程处理,但线程安全需自行保证,且不适合有状态的操作(如累加器)。
案例7:自定义对象复杂遍历
假设有Person类(name, age, city),需求:获取年龄>25且来自北京的人名,并按年龄降序。
List<Person> people = ...;
people.stream()
.filter(p -> p.getAge() > 25 && "北京".equals(p.getCity()))
.sorted(Comparator.comparingInt(Person::getAge).reversed())
.map(Person::getName)
.collect(Collectors.toList());
一行代码完成了过滤、排序、提取、收集,而传统方式至少需要5-8行。
常见问题与避坑指南
- Stream只能使用一次:一个Stream执行了终端操作后就关闭了,不能重复使用。
- 惰性求值:中间操作(如filter、map)不会立即执行,只有遇到终端操作(如collect、forEach)才触发。
- 并行流并非万能:数据量小、有顺序要求、存在共享变量时不宜用并行流。
- 空指针:集合本身为null会直接抛NPE,一定要先判空。
Q&A:你问我答
Q1:Stream遍历和for循环哪个性能更好?
A:对于小数据量(几千条以内),for循环略快(Stream有对象创建开销),但对于大数据量,尤其是并行流场景,Stream性能明显占优,且Stream代码更简洁,可读性更高。
Q2:Stream可以处理Map吗?
A:可以!map.entrySet().stream()再将Entry转为Stream,然后对key或value进行各种操作。
Q3:Stream中如何将结果转为数组?
A:使用toArray()方法:names.stream().toArray(String[]::new)。
Q4:是否可以嵌套遍历?
A:可以,但建议用flatMap展平流,避免嵌套循环,例如listOfLists.stream().flatMap(List::stream)。
Q5:Stream是否支持跳过和限制?
A:支持!skip(n)跳过前n个,limit(n)限制只取前n个,常用于分页场景。
Q6:遇到“stream has already been operated upon or closed”怎么解决?
A:每次操作后重建Stream,或使用SupplierSupplier<Stream<String>> streamSupplier = () -> names.stream();。
Stream是Java函数式编程的核心工具,通过7个实操案例,你已经掌握了从基础遍历到复杂自定义对象处理的全部技巧,先创建流,中间操作链式编排,最后终端操作输出结果,多加练习,你的代码会越来越“Stream化”!
延伸阅读:如果你想深入Stream底层原理,推荐阅读《Java 8实战》或查阅Oracle官方API文档。