Stream API
问题
Java Stream API 有哪些常用操作?中间操作和终端操作有什么区别?并行流需要注意什么?
答案
Stream 基础
Stream 是 Java 8 引入的集合数据处理管道,支持函数式风格的链式操作:
StreamBasic.java
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve");
// 过滤 → 转换 → 收集
List<String> result = names.stream()
.filter(name -> name.length() > 3) // 中间操作:过滤
.map(String::toUpperCase) // 中间操作:转换
.sorted() // 中间操作:排序
.collect(Collectors.toList()); // 终端操作:收集
// [ALICE, CHARLIE, DAVID]
Stream 的特点
- 惰性求值:中间操作不会立即执行,直到遇到终端操作才触发
- 一次性:Stream 只能消费一次,不能重复使用
- 不修改源数据:所有操作都生成新的 Stream
中间操作 vs 终端操作
| 分类 | 方法 | 说明 |
|---|---|---|
| 中间操作 | filter(Predicate) | 过滤 |
map(Function) | 一对一转换 | |
flatMap(Function) | 一对多转换(展平) | |
sorted() / sorted(Comparator) | 排序 | |
distinct() | 去重(基于 equals/hashCode) | |
limit(n) / skip(n) | 截取/跳过 | |
peek(Consumer) | 调试(不修改元素) | |
| 终端操作 | collect(Collector) | 收集到集合 |
forEach(Consumer) | 遍历 | |
reduce(BinaryOperator) | 归约 | |
count() | 计数 | |
anyMatch/allMatch/noneMatch | 匹配判断 | |
findFirst/findAny | 查找元素 | |
toArray() | 转为数组 |
常用操作示例
StreamOperations.java
List<Person> people = List.of(
new Person("Alice", 30, "Engineering"),
new Person("Bob", 25, "Marketing"),
new Person("Charlie", 35, "Engineering"),
new Person("David", 28, "Marketing"),
new Person("Eve", 32, "Engineering")
);
// flatMap:展平嵌套集合
List<List<Integer>> nested = List.of(List.of(1, 2), List.of(3, 4));
List<Integer> flat = nested.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList()); // [1, 2, 3, 4]
// reduce:归约
int sum = List.of(1, 2, 3, 4, 5).stream()
.reduce(0, Integer::sum); // 15
Optional<Integer> max = List.of(1, 2, 3).stream()
.reduce(Integer::max); // Optional[3]
// groupingBy:分组
Map<String, List<Person>> byDept = people.stream()
.collect(Collectors.groupingBy(Person::department));
// groupingBy + counting:分组计数
Map<String, Long> countByDept = people.stream()
.collect(Collectors.groupingBy(Person::department, Collectors.counting()));
// partitioningBy:二分(按条件分为两组)
Map<Boolean, List<Person>> partition = people.stream()
.collect(Collectors.partitioningBy(p -> p.age() > 30));
// joining:字符串连接
String names = people.stream()
.map(Person::name)
.collect(Collectors.joining(", ", "[", "]")); // [Alice, Bob, Charlie, David, Eve]
// toMap:转为 Map
Map<String, Integer> nameAge = people.stream()
.collect(Collectors.toMap(Person::name, Person::age));
// summarizingInt:统计信息
IntSummaryStatistics stats = people.stream()
.collect(Collectors.summarizingInt(Person::age));
stats.getAverage(); // 30.0
stats.getMax(); // 35
stats.getMin(); // 25
stats.getSum(); // 150
stats.getCount(); // 5
并行流
ParallelStream.java
// 创建并行流
List<Integer> numbers = IntStream.rangeClosed(1, 1000000)
.boxed().collect(Collectors.toList());
// 方式一:直接创建
long count = numbers.parallelStream()
.filter(n -> n % 2 == 0)
.count();
// 方式二:从串行流转换
long count = numbers.stream()
.parallel()
.filter(n -> n % 2 == 0)
.count();
并行流注意事项
- 线程安全:不要在并行流中修改共享变量
- 数据源:ArrayList 并行效率高(可拆分),LinkedList 效率低
- 操作类型:无状态操作(filter、map)适合并行,有状态操作(sorted、distinct)不适合
- 数据量:小数据量并行可能更慢(线程创建/合并开销)
- ForkJoinPool:并行流默认使用公共的 ForkJoinPool,长时间操作可能阻塞其他并行任务
常见面试问题
Q1: Stream 和 Collection 的区别?
答案:
| 维度 | Collection | Stream |
|---|---|---|
| 用途 | 存储数据 | 处理数据 |
| 执行时机 | 立即执行 | 惰性求值 |
| 可复用 | 可以多次遍历 | 只能消费一次 |
| 修改数据 | 可以增删改 | 不修改源数据 |
| 优化 | — | 短路优化、并行化 |
Q2: map() 和 flatMap() 的区别?
答案:
map():一对一,将每个元素转换为另一个元素flatMap():一对多,将每个元素转换为一个 Stream,然后把所有 Stream 合并(展平)
// map:每个字符串转为字符数组 → Stream<String[]>
List<String[]> result = words.stream()
.map(word -> word.split(""))
.collect(Collectors.toList()); // [["H","i"], ["B","y","e"]]
// flatMap:展平 → Stream<String>
List<String> result = words.stream()
.flatMap(word -> Arrays.stream(word.split("")))
.collect(Collectors.toList()); // ["H","i","B","y","e"]
Q3: Stream 是惰性求值吗?怎么理解?
答案:
是的。中间操作只是记录操作链,不会立即执行。直到遇到终端操作时,才从头执行所有操作:
// 不会打印任何东西!因为没有终端操作
Stream.of(1, 2, 3, 4, 5)
.filter(n -> {
System.out.println("filter: " + n);
return n > 2;
})
.map(n -> {
System.out.println("map: " + n);
return n * 2;
});
// 加上 .collect(Collectors.toList()) 才会执行
而且 Stream 会进行短路优化:findFirst()、anyMatch() 找到结果后立即停止处理。
Q4: Collectors.toList() 和 Stream.toList() 的区别?
答案:
| 维度 | Collectors.toList() | Stream.toList()(JDK 16+) |
|---|---|---|
| 结果 | 可变 ArrayList | 不可变 List |
| null 元素 | 允许 | 允许 |
| 版本 | JDK 8+ | JDK 16+ |
Q5: 什么情况下不适合使用 Stream?
答案:
- 简单遍历:普通 for 循环更直观
- 需要修改元素:Stream 不应有副作用
- 性能敏感:Stream 有创建对象和方法调用的开销
- 复杂逻辑:多层嵌套的 Stream 可读性反而差
- 小数据量的并行流:线程开销远大于计算开销