reduce and Aggregation

Combine a stream into one value with sum, max, summaryStatistics or reduce, and get money totals, empty streams and overflow right.

Syntaxint total = stream.mapToInt(X::n).sum(); Optional<T> max = stream.max(Comparator.comparing(X::n)); T result = stream.reduce(identity, (a, b) -> combine(a, b));

Aggregation turns many values into one: a total, a maximum, an average. Java gives you specialised operations for the common cases and reduce for the rest. Reach for the specialised ones first; they say what they do.

Built-in aggregations

  • On IntStream, LongStream and DoubleStream: sum(), min(), max(), average(), count() and summaryStatistics(), which gathers all of them in a single pass.
  • On any stream: count(), min(comparator) and max(comparator), which return an Optional of the element itself, so you get the whole record, not just the number.

Anything that can be empty returns an optional: max gives OptionalInt, average gives OptionalDouble. An empty cart has no average line price, and the type forces you to decide what to show.

reduce

BigDecimal total = lines.stream()
    .map(Line::total)
    .reduce(BigDecimal.ZERO, BigDecimal::add);

reduce(identity, accumulator) starts from the identity and folds each element in. The identity must really be neutral for the operation: 0 for addition, 1 for multiplication, BigDecimal.ZERO for money. reduce(accumulator) without an identity returns an Optional, empty for an empty stream. The operation must also be associative, because a parallel stream reduces chunks separately and then combines them. A wrong identity such as reduce(10, Integer::sum) looks fine sequentially but adds 10 once per chunk in parallel.

Money and overflow

Never total prices as double; use BigDecimal or whole cents in a long. mapToInt(...).sum() silently wraps around past about 2.1 billion, which is reachable for byte counts, cents across many orders or view counters. Use mapToLong for anything that grows, or reduce(0, Math::addExact) when overflow should fail loudly with an ArithmeticException.

Mistakes people make

  • Building strings with reduce("", String::concat). It creates a new string per element; use Collectors.joining.
  • Using reduce to fill a list or map. The accumulator must not mutate shared state; use collect.
  • Totalling in Java what SQL could total. For reports over large tables, SUM and AVG in the query avoid loading every row.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Total a shopping cart in BigDecimal so the charged amount matches the invoice to the cent.
  • Find the most expensive line of an order with max and a comparator to flag it for review.
  • Report count, min, max and average response time for a metrics endpoint with summaryStatistics.

More examples

Empty streams and optional results

Without an identity there is nothing to return for an empty stream, so you get an empty Optional. average() is always optional because the average of nothing is undefined.

Example · java
import java.util.List;

void main() {
    List<Integer> none = List.of();
    List<Integer> some = List.of(3, 4);

    IO.println(none.stream().reduce(Integer::sum));
    IO.println(none.stream().reduce(0, Integer::sum));
    IO.println(some.stream().reduce(Integer::sum));
    IO.println(none.stream().mapToInt(Integer::intValue).average());
    IO.println(some.stream().mapToInt(Integer::intValue).average().orElse(0));
}
// Output:
// Optional.empty
// 0
// Optional[7]
// OptionalDouble.empty
// 3.5

Overflow in int sums

Two sizes that fit in an int add up to more than Integer.MAX_VALUE, and the int sum wraps to a negative number with no error. Summing as long gives the right answer, and Math::addExact turns the overflow into an exception.

Example · java
import java.util.List;

void main() {
    List<Integer> fileSizes = List.of(1_500_000_000, 1_000_000_000);

    IO.println(fileSizes.stream().mapToInt(Integer::intValue).sum());
    IO.println(fileSizes.stream().mapToLong(Integer::longValue).sum());

    try {
        fileSizes.stream().reduce(0, Math::addExact);
    } catch (ArithmeticException e) {
        IO.println(e.getMessage());
    }
}
// Output:
// -1794967296
// 2500000000
// integer overflow

Latency summary in one pass

summaryStatistics collects count, sum, min, max and average in a single pass over the data. That is handy for a health or metrics endpoint that reports several figures at once.

Example · java
import java.util.IntSummaryStatistics;
import java.util.List;

void main() {
    List<Integer> latenciesMs = List.of(120, 80, 300, 950);

    IntSummaryStatistics stats = latenciesMs.stream()
        .mapToInt(Integer::intValue)
        .summaryStatistics();

    IO.println("count=" + stats.getCount() + " min=" + stats.getMin() + " max=" + stats.getMax());
    IO.println("avg=" + stats.getAverage());
}
// Output:
// count=4 min=80 max=950
// avg=362.5

Discussion

  • Be the first to comment on this lesson.