Java 8 Stream 面试题及深度解析

在Java后端开发的面试中,Java 8 Stream API 几乎是一个绕不开的话题。它不仅代表了Java函数式编程的开端,更是现代Java开发中处理集合数据的标准方式。本文将从面试官的角度,结合底层原理、代码实战以及常见陷阱,为您梳理一份详尽的 Java 8 Stream 面试题 指南。

一、 基础概念与核心特性

在深入代码之前,必须理解 Stream 的三个核心特性,这也是面试官常问的理论题:

  1. ① 不是数据结构:Stream 不会存储元素,它更像是连接元素源的管道。
  2. ② 函数式编程:操作不会修改源数据,而是产生一个新的结果(如 filter 不会删除原集合中的元素)。
  3. ③ 延迟执行:中间操作(Intermediate Operations)不会立即执行,直到终端操作(Terminal Operation)触发时才进行计算。

Stream 的操作流程

Step 1: 创建 Stream

通过集合、数组、IO 或生成器创建 Stream 实例。

Step 2: 中间操作

进行过滤、映射、排序等操作。此时仅构建操作链,不执行。

Step 3: 终端操作

触发执行,产生结果(如 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
Sort & Distinct
FlatMap & Peek

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 中最强大的聚合操作,有三种形式:

  1. ① Optional reduce(BinaryOperator accumulator):无初始值,返回 Optional。
  2. ② T reduce(T identity, BinaryOperator accumulator):有初始值,返回具体类型。
  3. ③ <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. 合理使用并行流,但需谨慎评估。