Java 8 Stream 面试题及深度解析
在Java后端开发的面试中,Java 8 Stream API 几乎是一个绕不开的话题。它不仅代表了Java函数式编程的开端,更是现代Java开发中处理集合数据的标准方式。本文将从面试官的角度,结合底层原理、代码实战以及常见陷阱,为您梳理一份详尽的 Java 8 Stream 面试题 指南。
一、 基础概念与核心特性
在深入代码之前,必须理解 Stream 的三个核心特性,这也是面试官常问的理论题:
- ① 不是数据结构:Stream 不会存储元素,它更像是连接元素源的管道。
- ② 函数式编程:操作不会修改源数据,而是产生一个新的结果(如 filter 不会删除原集合中的元素)。
- ③ 延迟执行:中间操作(Intermediate Operations)不会立即执行,直到终端操作(Terminal Operation)触发时才进行计算。
Stream 的操作流程
通过集合、数组、IO 或生成器创建 Stream 实例。
进行过滤、映射、排序等操作。此时仅构建操作链,不执行。
触发执行,产生结果(如 List、Optional 或 void)。执行后 Stream 流通常被关闭,不可复用。
二、 Stream 的常见创建方式
面试官常问:“你知道几种创建 Stream 的方式?” 以下是五种最常用的方式:
1. 通过 Collection 集合
最常用方式。调用 .stream() 或 .parallelStream()。
List list = Arrays.asList("a", "b", "c");
Stream stream = list.stream();
2. 通过 Arrays 数组
使用 Arrays.stream() 方法。
int[] nums = {1, 2, 3};
IntStream stream = Arrays.stream(nums);
3. 通过 Stream.of()
直接通过静态方法创建。
Stream stream = Stream.of("a", "b", "c");
4. 通过生成器 (generate)
生成无限流,需配合 limit 使用。
Stream randoms = Stream.generate(Math::random).limit(10);
5. 通过迭代 (iterate)
生成有序无限流。
Stream odds = Stream.iterate(1, n -> n + 2).limit(10);
三、 中间操作(Intermediate Operations)深度解析
中间操作返回的还是 Stream,因此可以链式调用。主要分为状态无关和状态有关两类。
Filter 与 Map
Filter:用于通过设置的条件过滤出元素。以下代码演示了过滤出长度大于3的字符串:
List words = Arrays.asList("Java", "Python", "C", "Go");
List result = words.stream()
.filter(s -> s.length() > 3)
.collect(Collectors.toList());
// 结果: [Java, Python]
Map:用于映射每个元素到对应的结果。例如将字符串转为大写:
List upper = words.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
面试题陷阱:Map 和 FlatMap 的区别?Map 是一对一,FlatMap 是一对多(展平)。
Sort 与 Distinct
Sorted:自然排序或自定义 Comparator 排序。注意它是状态有关的(Stateful),可能需要缓冲区。
list.stream().sorted(Comparator.comparingInt(User::getAge)).collect(...);
Distinct:去重,依赖对象的 equals() 和 hashCode() 方法。在对象去重时,务必重写这两个方法,否则去重可能失效。
FlatMap 与 Peek
FlatMap:将流中的每个元素替换为另一个流,然后将所有流连接成一个流。常用于二维集合扁平化。
List> nested = Arrays.asList(Arrays.asList(1,2), Arrays.asList(3,4));
List flat = nested.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
// 结果: [1, 2, 3, 4]
Peek:主要用于调试,对每个元素执行动作但不修改流。常用于查看流中元素的中间状态。
四、 终端操作(Terminal Operations)与归约
终端操作会触发实际计算,并关闭流。常见的包括 forEach, collect, reduce, findAny 等。
Collectors 工具类详解
Collectors 是归约操作的核心,提供了许多静态工厂方法。以下是面试中常考的归约场景:
| 方法 | 描述 | 示例代码片段 |
|---|---|---|
| toList() | 将流转换为 List | .collect(Collectors.toList()) |
| toSet() | 将流转换为 Set | .collect(Collectors.toSet()) |
| joining() | 字符串拼接 | .collect(Collectors.joining(", ")) |
| groupingBy() | 分组(核心难点) | .collect(Collectors.groupingBy(User::getDept)) |
| mapping() | 组合操作,先映射后收集 | .collect(Collectors.groupingBy(..., Collectors.mapping(...))) |
Reduce 操作:从入门到精通
reduce 是 Stream 中最强大的聚合操作,有三种形式:
- ①
Optional:无初始值,返回 Optional。reduce(BinaryOperator accumulator) - ②
T reduce(T identity, BinaryOperator:有初始值,返回具体类型。accumulator) - ③
<U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner):用于并行流,包含组合器。
// 计算数字总和
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
// 或者更简洁的写法
int sum = numbers.stream().mapToInt(Integer::intValue).sum();
五、 并行流(Parallel Stream)与性能陷阱
并行流利用 Fork/Join 框架,将任务拆分到多个线程中执行。虽然听起来很美好,但面试官通常会问:“什么时候不该用并行流?”
✅ 适合使用并行流的场景
- • 数据量非常大(百万级以上)。
- • 操作是纯函数式的,无副作用。
- • 计算密集型的操作(如复杂数学计算)。
- • 数据源本身支持快速拆分(如 ArrayList)。
❌ 避免使用并行流的场景
- • 数据量小(线程切换开销大于计算收益)。
- • 操作涉及 IO 或数据库连接(线程池耗尽)。
- • 共享可变状态(如
System.out.println导致输出乱序)。 - • 顺序依赖的操作(如
sorted在并行流中开销巨大)。
常见坑点:ConcurrentModificationException
在并行流中修改共享集合是线程不安全的。务必使用线程安全的集合(如 CopyOnWriteArrayList)或在流内部创建新集合。
六、 高级技巧与实战案例
在实际工作中,我们常需要处理复杂的业务逻辑。以下是几个高频实战场景。
场景1:List 转 Map
这是最高频的面试题之一。注意处理 Key 冲突的情况。
Map userMap = userList.stream()
.collect(Collectors.toMap(
User::getId,
user -> user,
(existing, replacement) -> existing // 解决Key冲突策略
));
场景2:对象列表去重
根据特定字段去重,例如根据用户名去重。
users = users.stream()
.collect(Collectors.collectingAndThen(
Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(User::getName))),
ArrayList::new
));
场景3:分页处理
虽然 Stream 本身不支持分页,但可以通过 skip 和 limit 模拟。
list.stream().skip((pageNum - 1) pageSize).limit(pageSize).collect(...);
七、 高频问答(FAQ)
Q: Stream 可以复用吗?
A: 不可以。一旦终端操作执行,Stream 流即被关闭(Closed),再次调用会抛出 IllegalStateException。
Q: Optional 类的作用是什么?
A: 用于避免 NullPointerException。提供 map, flatMap, orElse 等方法优雅处理空值。
Q: Stream 中的 forEach 与 for-each 循环有什么区别?
A: 语义上无太大区别,但 Stream 的 forEach 是终端操作,且支持并行流。传统 for-each 在循环内部 break/continue 更灵活,Stream 中需借助 filter 或 limit。
Q: 如何优化 Stream 的性能?
A: 1. 尽量使用基本类型流(IntStream, LongStream)避免装箱拆箱;2. 避免在流中使用耗时操作(如DB查询);3. 合理使用并行流,但需谨慎评估。