一句话总结
Stream 是 JDK 8 函数式数据处理 API,不存数据,惰性求值。操作分三类:创建 → 中间操作(filter/map/sorted,惰性)→ 终止操作(collect/forEach/reduce,触发计算)。短路操作(findFirst/limit)提前终止。并行流用 ForkJoinPool,适合 CPU 密集大数据量,注意线程安全和避免 IO。基本类型用 IntStream/LongStream/DoubleStream 避免装箱。
初级理解
Stream 是 JDK 8 引入的函数式数据处理API,用于对集合进行声明式的批量操作。它不是数据结构,不存储数据,只是对数据源的视图。
Stream 操作分为三类:
1. 创建 Stream:从集合、数组、文件等数据源创建
2. 中间操作(Intermediate):返回新的 Stream,惰性求值。如 filter、map、sorted、distinct、limit、skip、flatMap
3. 终止操作(Terminal):触发计算,返回结果或副作用。如 collect、forEach、reduce、count、anyMatch、findFirst
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
// 过滤长度>3的名字,转大写,排序,收集为List
List<String> result = names.stream()
.filter(name -> name.length() > 3) // 中间操作:过滤
.map(String::toUpperCase) // 中间操作:转换
.sorted() // 中间操作:排序
.collect(Collectors.toList()); // 终止操作:收集
System.out.println(result); // [ALICE, CHARLIE, DAVID]
中级深入
惰性求值(Lazy Evaluation):中间操作不会立即执行,只有在终止操作被调用时,整个流水线才会执行。这允许 Stream 进行优化,如短路操作。
// 惰性求值示例:findFirst 是短路操作
Optional<String> first = names.stream()
.filter(name -> {
System.out.println("filter: " + name);
return name.length() > 3;
})
.findFirst(); // 找到第一个就停止,不会处理所有元素
// 输出:filter: Alice → filter: Bob → filter: Charlie(找到后停止)
常用收集器(Collectors):
// 转 List/Set/Map
List<String> list = stream.collect(Collectors.toList());
Set<String> set = stream.collect(Collectors.toSet());
Map<Integer, String> map = stream.collect(
Collectors.toMap(String::length, Function.identity(), (a, b) -> a));
// 分组和分区
Map<Integer, List<String>> groups = stream.collect(
Collectors.groupingBy(String::length));
Map<Boolean, List<String>> partition = stream.collect(
Collectors.partitioningBy(s -> s.length() > 3));
// 统计
IntSummaryStatistics stats = stream.collect(
Collectors.summarizingInt(String::length));
高级拓展
并行流(Parallel Stream):通过 .parallelStream() 或 .parallel() 启用并行处理,底层使用 ForkJoinPool 的公共线程池(默认线程数 = CPU 核心数 - 1)。
// 并行流
long count = list.parallelStream()
.filter(s -> s.length() > 3)
.count();
并行流的注意事项:
1. 数据量大时才有效果,小数据量反而更慢(线程切换开销)
2. 操作必须是无状态的、线程安全的
3. 避免在并行流中使用同步块,会抵消并行优势
4. 不要用 parallelStream 执行 IO 操作(阻塞公共 ForkJoinPool)
Stream 与原始类型的性能:对于基本类型,应使用 IntStream、LongStream、DoubleStream 避免装箱开销。
// 避免装箱
IntStream.range(1, 100).sum(); // 高效
Stream.of(1, 2, 3).mapToInt(Integer::intValue).sum(); // 也 OK
面试加分项:能说出惰性求值、短路操作和并行流的适用场景与陷阱,说明你对 Stream 有深入理解。
实战场景
场景:Stream 实战常用模式
// 场景1:List 转 Map(处理重复 key)
Map<Long, User> userMap = users.stream()
.collect(Collectors.toMap(User::getId, Function.identity(),
(existing, replacement) -> replacement));
// 场景2:分组统计
Map<String, Long> countByDept = users.stream()
.collect(Collectors.groupingBy(User::getDept, Collectors.counting()));
// 场景3:多级分组
Map<String, Map<String, List<User>>> multiGroup = users.stream()
.collect(Collectors.groupingBy(User::getDept,
Collectors.groupingBy(User::getLevel)));
// 场景4:flatMap 扁平化(一对多)
List<String> allTags = articles.stream()
.flatMap(a -> a.getTags().stream())
.distinct()
.collect(Collectors.toList());
// 场景5:joining 拼接
String csv = users.stream()
.map(User::getName)
.collect(Collectors.joining(", "));
面试模拟
Q:map 和 flatMap 有什么区别?
A:map 是一对一映射(T → R),返回 Stream<R>;flatMap 是一对多映射(T → Stream<R>),将多个 Stream 扁平化为一个 Stream。典型场景:将 List<Order> 中每个 Order 的 List<Item> 展开为 Stream<Item>。
Q:Stream 和 for 循环怎么选?
A:简单遍历用 for(更直观),复杂数据处理用 Stream(filter+map+collect 链式调用更清晰)。Stream 的优势是声明式、可并行、链式组合;劣势是有 lambda 开销,简单循环比 for 稍慢。团队代码风格统一更重要。