Java 8 的 Stream 流是怎么用的?

2025年 阅读约 8 分钟 面试指南 · Java面试

深入解析Java 8 Stream流的使用和原理,从基本操作到并行流,分初级、中级、高级三个层次全面讲解。

一句话总结

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 稍慢。团队代码风格统一更重要。