Back to Stream Practice: Employee Analysis
Q9Question 9 of 43

Get DoubleSummaryStatistics of salaries (count, sum, min, max, average) using Collectors.summarizingDouble

Key Concepts
  • summarizingDouble(mapper) collects count, sum, min, max, and average into one DoubleSummaryStatistics object in a single pass
  • For empty streams, min is Double.POSITIVE_INFINITY and max is Double.NEGATIVE_INFINITY (sentinel values)
  • summarizingInt and summarizingLong are sibling collectors for primitive int and long keys
  • The statistics object is mutable and has a combine() method for parallel stream merging
  • For exact monetary totals, use BigDecimal reduce instead of summarizingDouble which has floating-point drift
  • groupingBy(department, summarizingDouble(salary)) gives per-department salary statistics

Definition

Collectors.summarizingDouble(Employee::getSalary) produces a DoubleSummaryStatistics object that tracks count, sum, min, max, and average of salaries in a single pass through the stream.
  • summarizingDouble(ToDoubleFunction) – collects stream elements into DoubleSummaryStatistics, tracking count, sum, min, max, and average simultaneously in a single pass.
  • getCount() – returns long, while getSum(), getMin(), getMax(), and getAverage() return double.
  • Empty stream – produces count=0, sum=0.0, average=0.0, min=Double.POSITIVE_INFINITY, and max=Double.NEGATIVE_INFINITY.
  • Siblings – summarizingInt produces IntSummaryStatistics and summarizingLong produces LongSummaryStatistics.
  • Per-group – groupingBy(department, summarizingDouble(salary)) gives statistics per department.

Priya at the National Bank of India needs a salary report card: how many employees, total payroll, lowest salary, highest salary, and average salary. Instead of walking the employee list five times (once per metric), she walks it once carrying a single notepad that updates all five numbers for every salary she sees. That notepad is DoubleSummaryStatistics.

Understanding the Concepts

What does this question ask?

The interviewer wants you to produce a complete statistical summary of a numeric field in one stream pass. This tests your knowledge of Collectors.summarizingDouble and the DoubleSummaryStatistics class.

How do you compute salary statistics in a single pass?

JAVA
package com.bank.hr;

import java.util.*;
import java.util.stream.Collectors;

public class SalarySummary {
    public static void main(String[] args) {
        List<Employee> employees = Employee.getSampleEmployees();

        DoubleSummaryStatistics stats = employees.stream()
            .collect(Collectors.summarizingDouble(
                e -> e.getSalary().doubleValue()));

        System.out.println("Count:   " + stats.getCount());
        System.out.printf("Sum:     Rs.%.2f%n", stats.getSum());
        System.out.printf("Min:     Rs.%.2f%n", stats.getMin());
        System.out.printf("Max:     Rs.%.2f%n", stats.getMax());
        System.out.printf("Average: Rs.%.2f%n", stats.getAverage());
    }
}
TEXT
Count:   10
Sum:     Rs.12200000.00
Min:     Rs.700000.00
Max:     Rs.1800000.00
Average: Rs.1220000.00

One pass produces five metrics. In the old for-loop approach, you would maintain separate count, sum, min, and max variables.

What happens with an empty stream?

JAVA
DoubleSummaryStatistics empty = List.<Employee>of().stream()
    .collect(Collectors.summarizingDouble(
        e -> e.getSalary().doubleValue()));

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());       // Infinity
System.out.println("Max:     " + empty.getMax());       // -Infinity
TEXT
Count:   0
Sum:     0.0
Average: 0.0
Min:     Infinity
Max:     -Infinity

The sentinel values Double.POSITIVE_INFINITY for min and Double.NEGATIVE_INFINITY for max are confusing on a dashboard. Always check stats.getCount() > 0 before using min and max.

How do you get per-department statistics?

Combine groupingBy with summarizingDouble as the downstream.

JAVA
package com.bank.hr;

import java.util.*;
import java.util.stream.Collectors;

public class StatsPerDepartment {
    public static void main(String[] args) {
        List<Employee> employees = Employee.getSampleEmployees();

        Map<String, DoubleSummaryStatistics> deptStats = employees.stream()
            .collect(Collectors.groupingBy(
                Employee::getDepartment,
                Collectors.summarizingDouble(
                    e -> e.getSalary().doubleValue())));

        deptStats.forEach((dept, s) ->
            System.out.printf("%s: count=%d, avg=Rs.%.0f, min=Rs.%.0f, max=Rs.%.0f%n",
                dept, s.getCount(), s.getAverage(), s.getMin(), s.getMax()));
    }
}
TEXT
TRADING: count=3, avg=Rs.1500000, min=Rs.1200000, max=Rs.1800000
RISK: count=3, avg=Rs.1200000, min=Rs.900000, max=Rs.1500000
OPERATIONS: count=2, avg=Rs.900000, min=Rs.700000, max=Rs.1100000
COMPLIANCE: count=2, avg=Rs.1050000, min=Rs.800000, max=Rs.1300000

How is summarizingDouble different from summarizingInt and summarizingLong?

| Collector | Mapper type | Stats class | Sum type | Use for |
|---|---|---|---|---|
| summarizingInt | ToIntFunction | IntSummaryStatistics | long | age, count, years |
| summarizingLong | ToLongFunction | LongSummaryStatistics | long | timestamps, large counts |
| summarizingDouble | ToDoubleFunction | DoubleSummaryStatistics | double | salary, percentage, rating |

JAVA
// summarizingInt for age
IntSummaryStatistics ageStats = employees.stream()
    .collect(Collectors.summarizingInt(Employee::getAge));

// summarizingDouble for salary (BigDecimal converted to double)
DoubleSummaryStatistics salaryStats = employees.stream()
    .collect(Collectors.summarizingDouble(
        e -> e.getSalary().doubleValue()));

What are common mistakes?

Mistake 1: not handling the empty stream sentinel values.

JAVA
// WRONG: displaying min/max without checking count
DoubleSummaryStatistics stats = getEmployees().stream()
    .collect(Collectors.summarizingDouble(e -> e.getSalary().doubleValue()));
System.out.println("Min salary: " + stats.getMin());  // shows Infinity for empty!

// CORRECT: guard with count check
if (stats.getCount() > 0) {
    System.out.println("Min salary: Rs." + stats.getMin());
} else {
    System.out.println("No employees to summarize");
}

Mistake 2: using summarizingDouble for exact money totals.

JAVA
// WRONG for audited figures: floating-point drift
DoubleSummaryStatistics approx = employees.stream()
    .collect(Collectors.summarizingDouble(
        e -> e.getSalary().doubleValue()));
// Sum might be 12199999.999999998 instead of 12200000

// CORRECT for exact totals: use BigDecimal reduce
import java.math.BigDecimal;
BigDecimal exactSum = employees.stream()
    .map(Employee::getSalary)
    .reduce(BigDecimal.ZERO, BigDecimal::add);

What about the mapToDouble + summaryStatistics alternative?

You can also get the same statistics object using mapToDouble followed by summaryStatistics().

JAVA
// Using Collectors.summarizingDouble
DoubleSummaryStatistics via1 = employees.stream()
    .collect(Collectors.summarizingDouble(
        e -> e.getSalary().doubleValue()));

// Using mapToDouble + summaryStatistics
DoubleSummaryStatistics via2 = employees.stream()
    .mapToDouble(e -> e.getSalary().doubleValue())
    .summaryStatistics();

// Both produce the same result

The mapToDouble approach is slightly more efficient because it avoids boxing: the stream becomes a DoubleStream of primitives. However, only the Collectors.summarizingDouble approach works as a downstream inside groupingBy.

What are common interview follow-up questions?

  • "How do you combine statistics from two separate streams?" Use stats1.combine(stats2) which merges the counts and sums.
  • "Can you create a custom summary that tracks median?" No, DoubleSummaryStatistics does not track median. You need a sorted list or a custom collector for that.
  • "How do you format the statistics as a report?" Use String.format or printf with the getter methods.

Visual Overview

DIAGRAM
  Stream<Employee>
  [700K, 900K, 1000K, 1100K, 1200K, 1300K, 1400K, 1500K, 1600K, 1800K]
       │
       │  .collect(summarizingDouble(salary))
       ▼
  ┌──────────────────────────────────────┐
  │  DoubleSummaryStatistics             │
  │                                      │
  │  count   = 10                        │
  │  sum     = 12,200,000.00             │
  │  min     = 700,000.00                │
  │  max     = 1,800,000.00              │
  │  average = 1,220,000.00              │
  └──────────────────────────────────────┘

  Empty stream sentinel values:
  count=0, sum=0.0, avg=0.0
  min=+Infinity, max=-Infinity  ← watch out!

Key Takeaway

Collectors.summarizingDouble produces a DoubleSummaryStatistics object that tracks count, sum, min, max, and average in a single pass through the stream. One traversal replaces five separate computations and produces a consistent snapshot of all metrics at once.

The critical gotcha is the empty stream case: count and sum are zero, but min is positive infinity and max is negative infinity. These sentinel values look confusing on a dashboard, so always check stats.getCount() > 0 before displaying min and max.

For per-department statistics, combine with groupingBy: groupingBy(department, summarizingDouble(salary)) produces a Map where each department maps to its own DoubleSummaryStatistics. For exact monetary totals in audited reports, use BigDecimal.reduce instead of summarizingDouble, which is subject to floating-point drift.

Remember one sentence: summarizingDouble gives count, sum, min, max, and average in one pass, but check for empty streams because min defaults to positive infinity.

---

Share
Use ← → arrow keys to navigate