reduce and Aggregation
Combine a stream into one value with sum, max, summaryStatistics or reduce, and get money totals, empty streams and overflow right.
int 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,LongStreamandDoubleStream:sum(),min(),max(),average(),count()andsummaryStatistics(), which gathers all of them in a single pass. - On any stream:
count(),min(comparator)andmax(comparator), which return anOptionalof 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; useCollectors.joining. - Using
reduceto fill a list or map. The accumulator must not mutate shared state; usecollect. - Totalling in Java what SQL could total. For reports over large tables,
SUMandAVGin the query avoid loading every row.
Example
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.
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.5Overflow 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.
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 overflowLatency 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.
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