Definition
- summaryStatistics() is a terminal operation on a primitive stream that returns a statistics object.
- DoubleSummaryStatistics holds count, sum, min, max, and average computed in one pass.
- IntSummaryStatistics and LongSummaryStatistics serve IntStream and LongStream respectively.
- Collectors.summarizingDouble() produces the same statistics object inside a collect, useful as a downstream in groupingBy.
Instead of running through the salary ledger five separate times to get the head count, total, smallest, largest, and average, Priya at the National Bank of India makes one pass and fills in all five boxes on a summary slip at once.
Understanding the Concepts
What is summaryStatistics?
summaryStatistics() is a terminal operation on a primitive stream that computes five numeric measures in a single traversal: count, sum, minimum, maximum, and average. It returns a DoubleSummaryStatistics object (or IntSummaryStatistics or LongSummaryStatistics for the corresponding stream types).
This is both faster and clearer than computing each statistic with a separate stream, because each separate stream would traverse the data from scratch.
package com.bank.hr;
import java.util.*;
import java.util.DoubleSummaryStatistics;
public class SalarySummary {
public static void main(String[] args) {
List<Employee> employees = List.of(
new Employee("Rajesh", "Loans", 75000.00),
new Employee("Priya", "Loans", 82000.00),
new Employee("Amit", "IT", 68000.00),
new Employee("Kavita", "IT", 91000.00),
new Employee("Neha", "Finance", 78000.00)
);
DoubleSummaryStatistics stats = employees.stream()
.mapToDouble(Employee::getSalaryAsDouble)
.summaryStatistics();
System.out.println("Count: " + stats.getCount());
System.out.println("Sum: " + stats.getSum());
System.out.println("Min: " + stats.getMin());
System.out.println("Max: " + stats.getMax());
System.out.println("Average: " + stats.getAverage());
}
}
Count: 5
Sum: 394000.0
Min: 68000.0
Max: 91000.0
Average: 78800.0
How is this different from calling average(), sum(), min() separately?
Each terminal operation consumes the stream, so you cannot call sum() and then average() on the same stream. With separate operations you must create a new stream each time, traversing the data multiple times.
| Approach | Passes | Streams created | Result |
|---|---|---|---|
| summaryStatistics() | 1 | 1 | All five statistics |
| Separate sum(), average(), min(), max(), count() | 5 | 5 | Same five statistics |
// WRONG: stream already consumed after sum()
var ds = employees.stream().mapToDouble(Employee::getSalaryAsDouble);
double sum = ds.sum();
double avg = ds.average().orElse(0); // IllegalStateException!
// CORRECT: one pass with summaryStatistics
var stats = employees.stream()
.mapToDouble(Employee::getSalaryAsDouble)
.summaryStatistics();
double sum2 = stats.getSum();
double avg2 = stats.getAverage();
How do you compute per-department summary statistics?
Collectors.summarizingDouble() produces the same DoubleSummaryStatistics object, but works inside a collect. Nest it as a downstream collector in groupingBy to get per-group statistics.
import java.util.stream.Collectors;
Map<String, DoubleSummaryStatistics> deptStats = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.summarizingDouble(Employee::getSalaryAsDouble)));
deptStats.forEach((dept, s) ->
System.out.printf("%s: count=%d, avg=%.0f, min=%.0f, max=%.0f%n",
dept, s.getCount(), s.getAverage(), s.getMin(), s.getMax()));
Loans: count=2, avg=78500, min=75000, max=82000
IT: count=2, avg=79500, min=68000, max=91000
Finance: count=1, avg=78000, min=78000, max=78000
What happens with an empty stream?
For an empty DoubleStream, summaryStatistics() returns an object with count 0, sum 0.0, average 0.0, but min as Double.MAX_VALUE and max as Double.MIN_VALUE (sentinel values). Always check getCount() before trusting min and max.
DoubleSummaryStatistics empty = java.util.stream.DoubleStream.empty()
.summaryStatistics();
System.out.println("Count: " + empty.getCount()); // 0
System.out.println("Sum: " + empty.getSum()); // 0.0
System.out.println("Average: " + empty.getAverage()); // 0.0
System.out.println("Min: " + empty.getMin()); // 1.7976931348623157E308
System.out.println("Max: " + empty.getMax()); // 4.9E-324
What are common mistakes?
Mistake: trusting min and max on an empty stream without checking count.
// WRONG: min is MAX_VALUE sentinel, not a real salary
var stats = List.<Employee>of().stream()
.mapToDouble(Employee::getSalaryAsDouble)
.summaryStatistics();
System.out.println("Min salary: " + stats.getMin());
// Prints: Min salary: 1.7976931348623157E308 (meaningless!)
// CORRECT: check count first
if (stats.getCount() > 0) {
System.out.println("Min salary: " + stats.getMin());
} else {
System.out.println("No employees to report");
}
Can you combine two DoubleSummaryStatistics objects?
Yes. The combine() method merges two statistics objects, which is how parallel streams work internally.
DoubleSummaryStatistics branch1 = new DoubleSummaryStatistics();
branch1.accept(75000.0);
branch1.accept(82000.0);
DoubleSummaryStatistics branch2 = new DoubleSummaryStatistics();
branch2.accept(68000.0);
branch2.accept(91000.0);
branch1.combine(branch2);
System.out.println("Combined count: " + branch1.getCount()); // 4
System.out.println("Combined avg: " + branch1.getAverage()); // 79000.0
What should a senior engineer know?
- summaryStatistics() is parallel-safe because the combine() method lets partial results merge.
- For exact money calculations, use BigDecimal reducing collectors, not summaryStatistics().
- The statistics object is mutable: accept() adds values and combine() merges. Do not share it across threads without synchronisation outside a stream context.
- Collectors.summarizingDouble() and stream.summaryStatistics() produce the same object type. The collector form is for groupingBy; the stream form is for standalone use.
What are common interview follow-up questions?
- "How do you get these statistics per department?" Use groupingBy with summarizingDouble as the downstream.
- "What is the time complexity?" O(n) for a single pass over n elements.
- "Is it thread-safe?" Within a parallel stream, yes. Outside a stream, the object is not synchronised.
Visual Overview
List<Employee>
[Rajesh:75K, Priya:82K, Amit:68K, Kavita:91K, Neha:78K]
│
│ .mapToDouble(Employee::getSalaryAsDouble)
▼
DoubleStream [75000, 82000, 68000, 91000, 78000]
│
│ .summaryStatistics() (single pass)
▼
┌──────────────────────────────────┐
│ DoubleSummaryStatistics │
│ count = 5 │
│ sum = 394000.0 │
│ min = 68000.0 │
│ max = 91000.0 │
│ average = 78800.0 │
└──────────────────────────────────┘
Key Takeaway
When a payroll report needs several numeric figures together, computing each with its own stream traverses the data multiple times. Instead, project salaries into a primitive DoubleStream with mapToDouble and call summaryStatistics(), which makes a single pass and returns a DoubleSummaryStatistics object holding count, sum, minimum, maximum, and average all at once.
This pairs well with Collectors.summarizingDouble() inside a groupingBy to compute per-department statistics in one pass. At the National Bank of India, one traversal for five figures is exactly the efficiency gain the dashboard team needed.
The trap to watch for is empty-stream behaviour: count, sum, and average are zero, but min and max come back as sentinel values (Double.MAX_VALUE and Double.MIN_VALUE). Always check getCount() before trusting the extremes.
Remember one sentence: summaryStatistics() gives count, sum, min, max, and average in one pass, and always check count before trusting min and max on an empty stream.
---