Back to Collectors
Q12Question 12 of 21

Collectors.reducing vs Stream.reduce: collector (downstream) vs terminal operation

Key Concepts
  • Stream.reduce is a terminal operation that folds the entire stream into a single value
  • Collectors.reducing is a Collector that performs the same fold but can be used as a downstream inside groupingBy
  • You cannot use Stream.reduce inside groupingBy: only Collectors work as downstream
  • Use Stream.reduce for whole-stream aggregation: stream.reduce(identity, op)
  • Use Collectors.reducing for per-group aggregation: groupingBy(key, reducing(identity, mapper, op))
  • The three-arg reducing(identity, mapper, operator) maps and reduces in one collector
  • For simple numeric sums prefer Collectors.summingInt/Long/Double over Collectors.reducing

Definition

Stream.reduce is a terminal operation for whole-stream aggregation; Collectors.reducing is a Collector for per-group aggregation inside groupingBy.
  • Stream.reduce(identity, BinaryOperator) – terminal operation that directly produces a single value from the entire stream.
  • Collectors.reducing(identity, mapper, BinaryOperator) – a Collector that can be used as a downstream inside groupingBy, partitioningBy, and other collector compositions.
  • Same logic – both perform a fold (accumulate elements into a single value), but in different contexts.
  • Rule – use Stream.reduce for the whole stream, Collectors.reducing for per-group aggregation.

Stream.reduce computes the bank-wide total. Collectors.reducing inside groupingBy computes the total per branch. Both do the same math, but one works on the whole ledger and the other on each branch's page.

Understanding the Concepts

What is the difference?

JAVA
import java.math.BigDecimal;
import java.util.Map;
import java.util.stream.Collectors;

// Stream.reduce: whole-stream total
BigDecimal bankTotal = trades.stream()
    .map(Trade::getAmount)
    .reduce(BigDecimal.ZERO, BigDecimal::add);

// Collectors.reducing: per-branch total inside groupingBy
Map<String, BigDecimal> branchTotals = trades.stream()
    .collect(Collectors.groupingBy(Trade::getBranch,
        Collectors.reducing(BigDecimal.ZERO, Trade::getAmount, BigDecimal::add)));
// {Mumbai: 500000, Delhi: 300000, Chennai: 200000}

Why can you not use Stream.reduce inside groupingBy?

JAVA
// WRONG: reduce is a terminal operation, not a Collector
// trades.stream().collect(groupingBy(Trade::getBranch, stream.reduce(...)));
// COMPILE ERROR!

// CORRECT: Collectors.reducing is a Collector
trades.stream().collect(Collectors.groupingBy(Trade::getBranch,
    Collectors.reducing(BigDecimal.ZERO, Trade::getAmount, BigDecimal::add)));

groupingBy expects a Collector as downstream. Stream.reduce is a terminal operation, not a Collector.

What are the three forms of Collectors.reducing?

JAVA
import java.util.Optional;
import java.util.stream.Collectors;

// Form 1: reducing(BinaryOperator) -> Optional<T>
Map<String, Optional<BigDecimal>> maxPerBranch = trades.stream()
    .collect(Collectors.groupingBy(Trade::getBranch,
        Collectors.reducing(BigDecimal::max)));
// Returns Optional because group might have no elements after mapping

// Form 2: reducing(identity, BinaryOperator) -> T
Map<String, BigDecimal> sumPerBranch = trades.stream()
    .collect(Collectors.groupingBy(Trade::getBranch,
        Collectors.mapping(Trade::getAmount,
            Collectors.reducing(BigDecimal.ZERO, BigDecimal::add))));

// Form 3: reducing(identity, mapper, BinaryOperator) -> U
// Maps each element before reducing. Combines map+reduce in one collector.
Map<String, BigDecimal> sumPerBranch2 = trades.stream()
    .collect(Collectors.groupingBy(Trade::getBranch,
        Collectors.reducing(BigDecimal.ZERO, Trade::getAmount, BigDecimal::add)));

When should you use each?

| Need | Use |
|---|---|
| Single value from entire stream | Stream.reduce |
| Per-group value inside groupingBy | Collectors.reducing |
| Simple numeric sum per group | Collectors.summingInt/Long/Double |
| Custom identity/operator per group | Collectors.reducing |
| Map and reduce in one step per group | 3-arg Collectors.reducing |

How does the three-arg Collectors.reducing work?

The three-arg form reducing(identity, mapper, operator) maps each element before reducing. This combines the map and reduce steps in a single collector.

JAVA
// Without three-arg: separate mapping and reducing
Map<String, BigDecimal> totals = trades.stream()
    .collect(Collectors.groupingBy(Trade::getBranch,
        Collectors.mapping(Trade::getAmount,
            Collectors.reducing(BigDecimal.ZERO, BigDecimal::add))));

// With three-arg: combined in one call
Map<String, BigDecimal> totals = trades.stream()
    .collect(Collectors.groupingBy(Trade::getBranch,
        Collectors.reducing(BigDecimal.ZERO, Trade::getAmount, BigDecimal::add)));
// Cleaner: identity=ZERO, mapper=getAmount, operator=add

What about Collectors.summingInt/Long/Double?

JAVA
// For simple numeric sums, summingInt/Long/Double is cleaner
Map<String, Long> countPerBranch = trades.stream()
    .collect(Collectors.groupingBy(Trade::getBranch, Collectors.counting()));

Map<String, Double> sumPerBranch = trades.stream()
    .collect(Collectors.groupingBy(Trade::getBranch,
        Collectors.summingDouble(Trade::getAmountDouble)));

// Use Collectors.reducing for BigDecimal or custom types
Map<String, BigDecimal> sumBD = trades.stream()
    .collect(Collectors.groupingBy(Trade::getBranch,
        Collectors.reducing(BigDecimal.ZERO, Trade::getAmount, BigDecimal::add)));

What are common mistakes?

JAVA
// WRONG: trying to use Stream.reduce as a downstream
// trades.stream().collect(groupingBy(Trade::getBranch, stream.reduce(...))); // ERROR

// WRONG: using reduce for per-group aggregation outside groupingBy
// You'd need to group first, then reduce each group manually

// CORRECT: Collectors.reducing as downstream
trades.stream().collect(Collectors.groupingBy(Trade::getBranch,
    Collectors.reducing(BigDecimal.ZERO, Trade::getAmount, BigDecimal::add)));

What should a senior engineer know?

  • Collectors.summingInt/Long/Double is syntactic sugar for simple numeric sums; prefer them for clarity.
  • The three-arg Collectors.reducing(identity, mapper, operator) maps each element before reducing, combining map+reduce in one collector.
  • Stream.reduce with three args (identity, accumulator, combiner) supports parallel reduction with type change.
  • For BigDecimal aggregation per group, Collectors.reducing is the only built-in option since there is no Collectors.summingBigDecimal.
  • Can Collectors.reducing be used standalone (not inside groupingBy)? Yes, but Stream.reduce is simpler for that case.

Visual Overview

DIAGRAM
  Stream.reduce (whole stream):
  Stream: [100, 200, 300] → reduce(0, add) → 600

  Collectors.reducing (per group):
  groupingBy(branch, reducing(0, amount, add)):
    Mumbai:  [100, 200] → 300
    Delhi:   [300]      → 300
    Chennai: [150, 50]  → 200
  Result: {Mumbai: 300, Delhi: 300, Chennai: 200}

Key Takeaway

Stream.reduce is a terminal operation for whole-stream aggregation. Collectors.reducing is a Collector for per-group aggregation inside groupingBy. Both perform the same fold logic, but in different contexts. You cannot use Stream.reduce inside groupingBy because it is a terminal operation, not a Collector.

Use Stream.reduce when you want a single value from the entire stream. Use Collectors.reducing when you need per-group reduction. For simple numeric sums, prefer Collectors.summingInt/Long/Double for clarity.

Remember one sentence: Stream.reduce aggregates the whole stream; Collectors.reducing aggregates per group inside groupingBy.

---

Share
Use ← → arrow keys to navigate