Collectors.summingInt() and averagingDouble()
Totals and averages a numeric field per stream or per group, and covers int overflow, the 0.0 average of an empty group and money.
public static <T> Collector<T, ?, Integer> summingInt(ToIntFunction<? super T> mapper)
public static <T> Collector<T, ?, Double> averagingDouble(ToDoubleFunction<? super T> mapper)- Added in Java Java 8
Parameters
| Parameter | Type | Description |
|---|---|---|
$mapper | ToIntFunction<? super T> (averagingDouble: ToDoubleFunction<? super T>) | Extracts the number to add up or average from each element. |
Return value
Collector<T, ?, Integer> (averagingDouble: Collector<T, ?, Double>)summingInt produces the int sum of the mapped values, 0 for no elements. averagingDouble produces the arithmetic mean of the mapped values, 0.0 for no elements.
Errors
A null mapper is not rejected when the collector is built; it throws NullPointerException as soon as an element is collected. summingInt does not detect overflow: a total above Integer.MAX_VALUE wraps around silently.
Collectors.summingInt adds up an int extracted from each element and Collectors.averagingDouble computes the mean of a double. Both belong to a family with one member per primitive type: summingInt, summingLong, summingDouble, and averagingInt, averagingLong, averagingDouble. They are most useful as downstream collectors, giving a total or an average per group:
Map<String, Integer> qtyByCategory = lines.stream()
.collect(Collectors.groupingBy(Line::category, Collectors.summingInt(Line::qty)));
Map<String, Double> avgRating = reviews.stream()
.collect(Collectors.groupingBy(Review::productId, Collectors.averagingDouble(Review::stars)));For a single total over the whole stream, mapToInt(Line::qty).sum() is shorter and avoids boxing; the collectors earn their place inside groupingBy and partitioningBy.
Result types
summingInt gives an Integer, summingLong a Long. Every averaging collector gives a Double, including averagingInt: the mean of 1 and 2 is 1.5, not 1.
Overflow
summingInt adds in an int and wraps around silently past about 2.1 billion. Summing amounts in cents, bytes transferred or milliseconds across a large data set can produce a negative total with no exception. If the total can grow large, use summingLong, which still accepts an int field because int widens to long.
The empty average is 0.0
An averaging collector over no elements returns 0.0, not NaN and not an empty Optional. A product with no reviews would show an average of zero stars. Inside groupingBy this never happens, because empty groups are not created, but it does happen with partitioningBy and on a whole stream. When you need to tell "no data" from "zero", use summarizingDouble and check getCount(), or mapToDouble(...).average(), which returns an OptionalDouble.
Money
Do not sum prices as double. Store money as whole cents in an int or long and use summingLong, or keep BigDecimal and reduce with Collectors.reducing(BigDecimal.ZERO, Line::price, BigDecimal::add). An average is a statistic, so a double is fine for it; round only when you display it.
Example
When to use it
- Totalling item quantities per warehouse from an order export to plan stock transfers.
- Computing the average star rating per product for a catalogue API, while showing 'no reviews' for products without any.
- Averaging response times per endpoint from access-log entries for a latency report.
Frequently asked questions
Why does Collectors.summingInt return a negative number?
The total went past Integer.MAX_VALUE (2,147,483,647) and wrapped around, because summingInt adds in an int and does not check for overflow. Use Collectors.summingLong, which accepts the same int values.
What does averagingDouble return for an empty stream?
0.0. It does not return NaN or an empty Optional. Use summarizingDouble and check getCount(), or mapToDouble(...).average(), which returns an OptionalDouble, when you need to tell 'no data' from zero.
Should I use summingInt or mapToInt().sum()?
For one total over a stream, mapToInt(...).sum() is shorter and works on primitives. Use summingInt where a Collector is needed, such as the downstream of groupingBy or partitioningBy.
More examples
summingInt overflow and summingLong
Two and a half billion does not fit in an int, so summingInt wraps to a negative number without any exception. summingLong takes the same int values and adds them as long.
import java.util.List;
import java.util.stream.Collectors;
void main() {
List<Integer> bytesPerFile = List.of(2_000_000_000, 500_000_000);
Integer wrong = bytesPerFile.stream().collect(Collectors.summingInt(b -> b));
Long right = bytesPerFile.stream().collect(Collectors.summingLong(b -> b));
IO.println("summingInt: " + wrong);
IO.println("summingLong: " + right);
}
// Output:
// summingInt: -1794967296
// summingLong: 2500000000Count, total, minimum, maximum and average in one pass
summarizingInt collects every common statistic at once, and getSum() is a long, so it does not overflow like summingInt. getCount() lets you report 'no data' instead of an average of zero.
import java.util.IntSummaryStatistics;
import java.util.List;
import java.util.stream.Collectors;
record Call(String path, int ms) {}
void main() {
List<Call> calls = List.of(
new Call("/orders", 120), new Call("/orders", 80), new Call("/orders", 100));
IntSummaryStatistics stats = calls.stream()
.collect(Collectors.summarizingInt(Call::ms));
IO.println("count=" + stats.getCount() + " sum=" + stats.getSum()
+ " min=" + stats.getMin() + " max=" + stats.getMax()
+ " avg=" + stats.getAverage());
IntSummaryStatistics empty = List.<Call>of().stream()
.collect(Collectors.summarizingInt(Call::ms));
IO.println(empty.getCount() == 0 ? "no data" : "avg=" + empty.getAverage());
}
// Output:
// count=3 sum=300 min=80 max=120 avg=100.0
// no dataAverage response time per endpoint
averagingInt takes int milliseconds but returns a Double, so 50 and 75 average to 62.5 rather than a truncated 62. Rounding happens only at display time, where String.format rounds half up and shows 63.
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
record Call(String path, int ms) {}
void main() {
List<Call> calls = List.of(
new Call("/orders", 120), new Call("/users", 50),
new Call("/orders", 80), new Call("/users", 75),
new Call("/orders", 100));
Map<String, Double> avgMs = calls.stream()
.collect(Collectors.groupingBy(Call::path, TreeMap::new,
Collectors.averagingInt(Call::ms)));
IO.println(avgMs);
avgMs.forEach((path, ms) -> IO.println(path + " " + String.format("%.0f", ms) + " ms"));
}
// Output:
// {/orders=100.0, /users=62.5}
// /orders 100 ms
// /users 63 ms
Discussion