在 Java Stream 中,reduce 和 collect 有什么区别?
在 Java Stream API 中,reduce 和 collect 是两个最核心的终端操作(Terminal Operations)。它们都用于将流中的元素组合成一个单一的结果,但它们的设计理念、工作原理和适用场景有本质区别。
简而言之:reduce 侧重于“不可变归约”(Immutable Reduction),而 collect 侧重于“可变归约”(Mutable Reduction)。
1. reduce(不可变归约)
核心概念:
reduce 旨在通过反复应用某个结合操作,将流中的元素合并为一个新的不可变对象。
每次操作都会产生一个新值,而不会修改原有的值(类似于 SQL 中的 SUM、MIN、MAX)。
特点:
- 不可变性:累加器(Accumulator)每次都返回一个新对象(如
String、Integer、BigDecimal等不可变类)。 - 返回值:通常返回
Optional<T>或直接返回计算结果T。
常用场景:
求和、求积、求最大/最小值、连接字符串(小规模)等。
代码示例:
java
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 示例 1:求和(有初始值 0)
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b); // 15
// 示例 2:求最大值(无初始值,返回 Optional)
Optional<Integer> max = numbers.stream()
.reduce(Integer::max); // Optional[5]
2. collect(可变归约)
核心概念:
collect 旨在将流中的元素收集到一个可变的容器中(如 List、Set、Map、StringBuilder 等)。
它不会每次都创建新容器,而是将元素追加(mutate)到同一个现有的容器中。
特点:
- 可变性:操作的是可变容器,效率极高(避免了频繁创建新对象的开销)。
- 容器化:配合
Collectors工具类可以实现极其复杂的聚合、分组和转换逻辑。
常用场景:
将流转换为 List/Set/Map、分组(groupingBy)、分区(partitioningBy)、字符串拼接(joining)等。
代码示例:
java
List<String> words = Arrays.asList("apple", "banana", "cherry");
// 示例 1:收集到 List 中
List<String> list = words.stream()
.filter(w -> w.startsWith("a"))
.collect(Collectors.toList()); // [apple]
// 示例 2:分组
Map<Integer, List<String>> groupedByLength = words.stream()
.collect(Collectors.groupingBy(String::length)); // {5=[apple], 6=[banana, cherry]}
3. 核心区别对比
| 特性 | reduce |
collect |
|---|---|---|
| 主要设计目的 | 得到一个不可变的单一结果(数值、布尔值等) | 得到一个可变的容器/集合结果(List、Set、Map等) |
| 累加机制 | 每次生成一个新对象(例如 a + b 产生新数) |
将元素放入已有容器(例如 list.add(element)) |
| 并行流性能 | 适用于轻量级的数据(如基本类型),若用于集合会产生大量对象拷贝,性能极差。 | 并行流性能极佳。多个线程分别收集到局部容器中,最后通过 combiner 合并容器。 |
| 典型返回值 | Optional<T>, T |
List<T>, Set<T>, Map<K, V>, String |
| 核心参数 | identity(初始值)accumulator(累加器)combiner(组合器,用于并行流) |
supplier(创建容器)accumulator(加入容器)combiner(合并容器) |
4. 为什么不能混用?(避坑指南)
错误示范:用 reduce 来收集 List
有人可能会写出这样的代码,尝试用 reduce 把元素装进 ArrayList:
java
// !!! 极其糟糕的写法 !!!
List<Integer> list = stream.reduce(
new ArrayList<Integer>(),
(l, e) -> { l.add(e); return l; }, // 违反了 reduce 的不可变契约
(l1, l2) -> { l1.addAll(l2); return l1; }
);
为什么糟糕?
- 线程安全问题:在并行流中,多个线程会并发修改同一个
ArrayList,导致数据竞争和未定义行为(reduce默认传入的初始值应该是个“恒等值”,每次结合不应修改原对象)。 - 违背设计直觉:这本质上是可变操作,应该使用
collect:javaList<Integer> list = stream.collect(ArrayList::new, List::add, List::addAll); // 或者更简单的: List<Integer> list = stream.collect(Collectors.toList());
总结建议
- 当你需要计算一个数值(求和、最大值、布尔判断等),或者处理的是基础数据类型/不可变对象时,优先选择
reduce。 - 当你需要整理数据结构(转为 List/Set/Map、拼接字符串、按属性分组等),需要将结果放入一个可变容器时,必须选择
collect。
右滑查看面试常问