1. reduce 方法:通用归约
在编程中,经常需要将集合“归约”为一个最终值:计算和、求乘积、拼接字符串、计算聚合指标,或者将元素收集到新的结构中。过去通常通过循环与累加变量手动完成。如今 Stream API 提供了优雅的方式——通用方法 reduce 与 collect,让代码更简洁、更具声明性。
- reduce —— 将流归约为一个最终值(和、乘积、拼接等)。
- collect —— 将流转换为集合、字符串、映射或任意结构。
我们按顺序来看看。
为什么需要 reduce?
reduce 是一个终止方法,它使用累加函数将流中的元素“归约”为一个值。可以把它想象为对集合逐步累积结果的一次遍历。
reduce 方法的签名
在 Stream API 中,reduce() 有三个主要变体:
Optional<T> reduce(BinaryOperator<T> accumulator)
T reduce(T identity, BinaryOperator<T> accumulator)
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)
- accumulator —— 一个函数,接收当前累积值与下一个元素,并返回新的结果。
- identity —— 累加器的初始值(例如,求和用 0,乘积用 1)。
- combiner —— 在并行流中用于合并中间结果。
reduce 的使用示例
示例 1:数字求和
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// reduce 不带 identity —— 结果为 Optional
Optional<Integer> sum1 = numbers.stream()
.reduce((a, b) -> a + b);
System.out.println(sum1.orElse(0)); // 15
// 带 identity 的 reduce —— 一定有结果
int sum2 = numbers.stream()
.reduce(0, (a, b) -> a + b);
System.out.println(sum2); // 15
示例 2:所有数字的乘积
int product = numbers.stream()
.reduce(1, (a, b) -> a * b);
System.out.println(product); // 120
示例 3:连接字符串
List<String> words = List.of("Java", "Stream", "API");
String phrase = words.stream()
.reduce("", (a, b) -> a + " " + b);
System.out.println(phrase.trim()); // Java Stream API
示例 4:查找最大元素
Optional<Integer> max = numbers.stream()
.reduce(Integer::max);
max.ifPresent(System.out::println); // 5
示例 5:所有字符串长度之和
List<String> texts = List.of("cat", "dog", "elephant");
int totalLength = texts.stream()
.map(String::length)
.reduce(0, Integer::sum);
System.out.println(totalLength); // 14
reduce 如何工作
reduce 的逻辑等价于如下循环:
T result = identity;
for (T element : collection) {
result = accumulator.apply(result, element);
}
return result;
如果未指定 identity,则起始值取流的第一个元素,方法返回 Optional(若流为空则为 empty)。
2. collect 方法:通用转换
collect 是一个终止方法,用于将流转换为集合、字符串、映射或任意其他结构。为此需要“收集器”(Collector)来描述收集过程。最常见的是直接使用 Collectors 类提供的现成收集器。
最常用的收集器
- Collectors.toList() —— 将元素收集到 List。
- Collectors.toSet() —— 将元素收集到 Set。
- Collectors.toMap() —— 将元素收集到 Map。
- Collectors.joining() —— 将多个字符串拼接为一个。
- Collectors.groupingBy() —— 按条件对元素分组。
- Collectors.counting() —— 统计元素个数。
- Collectors.summarizingInt() —— 汇总数值统计(和、平均、最小/最大)。
collect 的使用示例
示例 1:收集为列表
List<String> names = List.of("安雅", "鲍里斯", "瓦夏", "安雅");
List<String> uniqueNames = names.stream()
.distinct()
.collect(Collectors.toList());
System.out.println(uniqueNames); // [安雅, 鲍里斯, 瓦夏]
示例 2:收集为集合(Set)
Set<String> nameSet = names.stream()
.collect(Collectors.toSet());
System.out.println(nameSet); // [安雅, 鲍里斯, 瓦夏](顺序不保证)
示例 3:收集为字符串
String csv = names.stream()
.collect(Collectors.joining(", "));
System.out.println(csv); // 安雅, 鲍里斯, 瓦夏, 安雅
示例 4:收集为 Map
假设有如下类:
public class Employee {
private String name;
private String department;
public Employee(String name, String department) {
this.name = name;
this.department = department;
}
public String getName() { return name; }
public String getDepartment() { return department; }
}
收集为“姓名 → 部门”的映射:
List<Employee> employees = List.of(
new Employee("安雅", "IT"),
new Employee("鲍里斯", "HR"),
new Employee("瓦夏", "IT")
);
Map<String, String> nameToDept = employees.stream()
.collect(Collectors.toMap(
Employee::getName,
Employee::getDepartment,
(oldValue, newValue) -> newValue // 处理重复的姓名
));
System.out.println(nameToDept); // {安雅=IT, 鲍里斯=HR, 瓦夏=IT}
示例 5:将唯一元素收集到 Set
Set<String> unique = names.stream()
.collect(Collectors.toSet());
System.out.println(unique);
示例 6:汇总数值统计
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
IntSummaryStatistics stats = numbers.stream()
.collect(Collectors.summarizingInt(Integer::intValue));
System.out.println(stats.getSum()); // 15
System.out.println(stats.getAverage()); // 3.0
System.out.println(stats.getMax()); // 5
System.out.println(stats.getMin()); // 1
3. 对比:何时使用 reduce,何时使用 collect
当需要通过二元操作得到一个最终值(和、乘积、最大值、字符串拼接)时,用 reduce。
当需要将元素收集到集合/映射/字符串,或借助 Collector 完成更复杂的聚合时,用 collect。对于这些任务,collect 通常更强大也更高效。
表格:reduce vs collect
| 任务 | 该用什么 | 示例 |
|---|---|---|
| 数字求和 | |
|
| 乘积 | |
|
| 收集到 List | |
|
| 收集到 Map | |
|
| 分组 | |
|
| 字符串连接 | reduce / collect | reduce("", String::concat) 或 Collectors.joining() |
4. 实战练习
练习 1:求列表中所有字符串长度之和
List<String> words = List.of("cat", "dog", "elephant");
int totalLength = words.stream()
.mapToInt(String::length)
.sum(); // 或使用 reduce:.reduce(0, Integer::sum)
System.out.println(totalLength); // 14
练习 2:将唯一元素收集到 Set
List<String> fruits = List.of("苹果", "梨", "苹果", "橙子");
Set<String> uniqueFruits = fruits.stream()
.collect(Collectors.toSet());
System.out.println(uniqueFruits); // [苹果, 梨, 橙子]
练习 3:从对象列表构建 Map
List<Employee> employees = List.of(
new Employee("安雅", "IT"),
new Employee("鲍里斯", "HR"),
new Employee("瓦夏", "IT")
);
Map<String, String> nameToDept = employees.stream()
.collect(Collectors.toMap(
Employee::getName,
Employee::getDepartment,
(oldValue, newValue) -> newValue // 如果姓名相同
));
System.out.println(nameToDept);
练习 4:用逗号拼接所有姓名
String allNames = employees.stream()
.map(Employee::getName)
.collect(Collectors.joining(", "));
System.out.println(allNames); // 安雅, 鲍里斯, 瓦夏
5. 实现要点与注意事项
Optional 与 reduce
若使用不带 identity 的 reduce,返回结果是 Optional。这更安全:当流为空时,结果也为空。请正确处理它:ifPresent(...)、orElse(...)、orElseThrow(...)。
Optional<Integer> max = numbers.stream().reduce(Integer::max);
max.ifPresent(System.out::println);
自定义收集器:当现成的还不够
如果标准收集器不满足需求,你可以编写自己的 Collector。但对 99% 的场景来说,Collectors 中的现成收集器已经足够。
收集器与并行流
Collectors 提供的收集器被设计为可在 parallelStream() 中正确工作。不要在并行流的 forEach 中向共享的可变集合手动添加元素——会遇到数据竞争。
6. 使用 reduce 和 collect 的常见错误
错误 1:在 reduce 之后不检查 Optional。 如果流为空,不带 identity 的 reduce 会返回空的 Optional。直接调用 get() 会导致 NoSuchElementException。请使用 ifPresent、orElse 或 orElseThrow。
错误 2:尝试通过 reduce 来收集集合。 虽然能做到,但 collect 更适合且更高效:
// 低效的做法!
List<String> list = stream.reduce(
new ArrayList<>(),
(acc, elem) -> { acc.add(elem); return acc; },
(acc1, acc2) -> { acc1.addAll(acc2); return acc1; }
);
// 更好的方式:
List<String> list2 = stream.collect(Collectors.toList());
错误 3:在 toMap 中没有处理重复键。 如果键重复,会抛出异常。请在 toMap 中提供第三个参数以解决冲突。
错误 4:在并行流中对可变集合进行非同步操作。 在 collect 中使用标准收集器——它们能在并行模式下正确工作。不要在 parallelStream() 上的 forEach 中执行 list.add()。
错误 5:在复杂任务中混用 reduce 与 collect。 reduce 适合简单的聚合(和、最大值)。collect 适合收集到集合、分组、构建 Map 与复杂聚合。
GO TO FULL VERSION