CodeGym /课程 /JAVA 25 SELF /reduce 和 collect 方法:数据聚合

reduce 和 collect 方法:数据聚合

JAVA 25 SELF
第 31 级 , 课程 1
可用

1. reduce 方法:通用归约

在编程中,经常需要将集合“归约”为一个最终值:计算和、求乘积、拼接字符串、计算聚合指标,或者将元素收集到新的结构中。过去通常通过循环与累加变量手动完成。如今 Stream API 提供了优雅的方式——通用方法 reducecollect,让代码更简洁、更具声明性。

  • 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

任务 该用什么 示例
数字求和
reduce
reduce(0, Integer::sum)
乘积
reduce
reduce(1, (a, b) -> a * b)
收集到 List
collect
collect(Collectors.toList())
收集到 Map
collect
collect(Collectors.toMap(...))
分组
collect
collect(Collectors.groupingBy(...))
字符串连接 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

若使用不带 identityreduce,返回结果是 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。 如果流为空,不带 identityreduce 会返回空的 Optional。直接调用 get() 会导致 NoSuchElementException。请使用 ifPresentorElseorElseThrow

错误 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 与复杂聚合。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION