Back to Stream: Grouping & Partitioning
Q9Question 9 of 22

Compute the average salary per department correctly, contrasting averagingDouble with a BigDecimal average

Key Concepts
  • averagingDouble(mapper) returns the arithmetic mean as a Double, tracking sum and count in one pass.
  • groupingBy(dept, averagingDouble(getSalary)) gives Map<String, Double> of average salary per department.
  • double cannot represent many decimals exactly, so it is wrong for currency averages.
  • The correct money average sums BigDecimal values and divides by the count with explicit scale and rounding.
  • BigDecimal.divide requires a scale and a RoundingMode, use HALF_EVEN for finance.
  • HALF_EVEN (banker's rounding) reduces cumulative bias compared with HALF_UP.
  • Guard the empty-group case because dividing by a zero count throws ArithmeticException.

Definition

`averagingDouble` gives a fast mean as a `double`; for money, sum `BigDecimal` values and divide by the count with an explicit scale and rounding mode.

Averaging a group has a fast path and a correct money path.

  • Collectors.averagingDouble(mapper) – returns the arithmetic mean of a double-valued property as a Double, computing sum and count in one pass.
  • Nested in groupingBy – groupingBy(dept, averagingDouble(getSalary)) gives Map per department.
  • Limitation for money – averaging in double risks rounding error that violates banking precision.
  • Correct money average – sum BigDecimal salaries exactly and divide by the count with an explicit scale and RoundingMode.
  • Rounding mode – HALF_EVEN (banker's rounding) reduces cumulative bias in finance.

Priya at the National Bank of India wants the average salary in each department. The quick way adds the salaries and divides by how many, using ordinary decimals, which is averagingDouble. But money needs an exact calculator, so the careful way adds exact rupee amounts and divides with a fixed number of decimal places and a chosen rounding rule.

Understanding the Concepts

What is Collectors.averagingDouble?

averagingDouble(mapper) maps each element to a double and returns the mean as a Double, internally tracking the running sum and count so it needs only one pass. Nested in groupingBy, it gives the average of a numeric field per group.

JAVA
package com.bank.finance;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class AverageSalary {
    public static Map<String, Double> fastAverageByDept(List<Employee> employees) {
        return employees.stream()
                .collect(Collectors.groupingBy(
                        Employee::getDepartment,
                        Collectors.averagingDouble(e -> e.getSalary().doubleValue())));
    }
}

How does an averaging collector work internally?

The collector keeps two running numbers per group: the sum of the mapped values and the count of elements. When the stream finishes, the finisher divides the sum by the count to produce the mean. Because both are tracked in one pass, there is no second traversal. On an empty group the collector yields zero rather than an error.

JAVA
// Conceptually averagingDouble keeps { double sum; long count; }
// accumulate: sum += mapper(element); count++;
// finish:     return count == 0 ? 0.0 : sum / count;

How do you run the fast averagingDouble example?

JAVA
package com.bank.finance;

import java.math.BigDecimal;

public class Employee {
    private final String name;
    private final String department;
    private final BigDecimal salary;

    public Employee(String name, String department, BigDecimal salary) {
        this.name = name;
        this.department = department;
        this.salary = salary;
    }

    public String getName() { return name; }
    public String getDepartment() { return department; }
    public BigDecimal getSalary() { return salary; }
}
JAVA
package com.bank.finance;

import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class AverageSalaryDemo {
    public static void main(String[] args) {
        List<Employee> employees = List.of(
                new Employee("Rajesh", "Trading", new BigDecimal("1200000")),
                new Employee("Priya", "Trading", new BigDecimal("1500000")),
                new Employee("Amit", "Trading", new BigDecimal("400000")),
                new Employee("Pankaj", "IT", new BigDecimal("900000")));

        Map<String, Double> avg = employees.stream()
                .collect(Collectors.groupingBy(
                        Employee::getDepartment,
                        Collectors.averagingDouble(e -> e.getSalary().doubleValue())));

        avg.forEach((dept, mean) -> System.out.println(dept + " -> " + mean));
    }
}
BASH
javac com/bank/finance/Employee.java com/bank/finance/AverageSalaryDemo.java
java com.bank.finance.AverageSalaryDemo
TEXT
IT -> 900000.0
Trading -> 1033333.3333333334

The Trading average carries the repeating decimal 1,033,333.3333333334, which is the floating-point approximation.

Why is averagingDouble wrong for currency?

double cannot represent many decimal fractions exactly, and the division compounds this, so an average like 1,033,333.3333 may carry tiny floating-point inaccuracy that violates banking precision rules. Money must be exact and controlled to a fixed scale.

JAVA
// double division produces an uncontrolled, approximate tail.
double mean = (1200000.0 + 1500000.0 + 400000.0) / 3;
System.out.println(mean); // 1033333.3333333334, scale and rounding uncontrolled

How do you compute a correct BigDecimal average per department?

Keep salaries as BigDecimal, sum them exactly, and divide by the count using BigDecimal.divide(count, scale, RoundingMode), which forces a defined number of decimal places and a deliberate rounding behaviour.

JAVA
package com.bank.finance;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class AccurateAverageSalary {
    public static Map<String, BigDecimal> averageByDept(List<Employee> employees) {
        return employees.stream()
                .collect(Collectors.groupingBy(
                        Employee::getDepartment,
                        Collectors.collectingAndThen(
                                Collectors.toList(),
                                group -> {
                                    BigDecimal total = group.stream()
                                            .map(Employee::getSalary)
                                            .reduce(BigDecimal.ZERO, BigDecimal::add);
                                    return total.divide(
                                            BigDecimal.valueOf(group.size()),
                                            2, RoundingMode.HALF_EVEN);
                                })));
    }
}
TEXT
IT -> 900000.00
Trading -> 1033333.33

How do the two approaches compare?

| Aspect | averagingDouble | BigDecimal average |
|--------|-----------------|--------------------|
| Result type | Double | BigDecimal |
| Precision | approximate, lossy | exact, controlled |
| Scale control | none | explicit via divide |
| Rounding control | none | explicit RoundingMode |
| Correct for money | no | yes |
| Good for | age, latency, metrics | salaries, ledger figures |

JAVA
// Fast but lossy: dashboard metric only.
Map<String, Double> fast = employees.stream()
        .collect(Collectors.groupingBy(Employee::getDepartment,
                Collectors.averagingDouble(e -> e.getSalary().doubleValue())));

// Authoritative: finance ledger figure.
Map<String, BigDecimal> exact = AccurateAverageSalary.averageByDept(employees);

Why use HALF_EVEN rounding in finance?

HALF_EVEN, known as banker's rounding, rounds a halfway value to the nearest even digit, which reduces cumulative bias compared with HALF_UP that always rounds halves away from zero. When many averages roll up into a total, HALF_UP drifts upward, while HALF_EVEN balances out.

JAVA
import java.math.BigDecimal;
import java.math.RoundingMode;

BigDecimal v = new BigDecimal("2.5");
System.out.println(v.setScale(0, RoundingMode.HALF_UP));   // 3
System.out.println(v.setScale(0, RoundingMode.HALF_EVEN)); // 2, rounds to even

BigDecimal w = new BigDecimal("3.5");
System.out.println(w.setScale(0, RoundingMode.HALF_EVEN)); // 4, rounds to even

What are common mistakes?

The biggest mistake is averaging money as a double. A second is calling BigDecimal.divide without a scale and rounding mode, which throws ArithmeticException when the result is a non-terminating decimal. A third is dividing by a zero count on an empty group.

JAVA
// WRONG: divide without scale and rounding throws on 2700000 / 3 style non-terminating results.
// total.divide(BigDecimal.valueOf(count)); // ArithmeticException: Non-terminating decimal expansion

// CORRECT: supply scale and rounding mode.
BigDecimal total = new BigDecimal("3100000");
BigDecimal average = total.divide(BigDecimal.valueOf(3), 2, RoundingMode.HALF_EVEN);
System.out.println(average); // 1033333.33

What are the edge cases?

An empty group never appears in a groupingBy result, so the divide-by-zero only arises if you compute an average outside grouping. averagingDouble yields 0.0 for an empty collector. Very large sums stay exact with BigDecimal but must still be given a scale on division.

JAVA
// Edge case: guard divide-by-zero when averaging a possibly-empty list directly.
List<Employee> group = List.of();
BigDecimal avg = group.isEmpty()
        ? BigDecimal.ZERO
        : group.stream().map(Employee::getSalary)
                .reduce(BigDecimal.ZERO, BigDecimal::add)
                .divide(BigDecimal.valueOf(group.size()), 2, RoundingMode.HALF_EVEN);

When should you use averagingDouble, and when not?

Use averagingDouble for non-monetary metrics like average age, average account tenure, or average request latency, where a double is an acceptable representation. Do not use it for money on an authoritative ledger, where you must control scale and rounding with BigDecimal.

JAVA
// Use averagingDouble: average account age in days, a metric.
Map<String, Double> avgAgeDays = employees.stream()
        .collect(Collectors.groupingBy(Employee::getDepartment,
                Collectors.averagingDouble(e -> e.getSalary().doubleValue()))); // metric only

// Do NOT use for the authoritative salary average: use BigDecimal path.

What are common interview follow-up questions?

Interviewers ask how averaging collectors track sum and count, why double is wrong for money, what scale and rounding mode to use, why HALF_EVEN, and how to guard divide-by-zero. A senior answer mentions showing a fast averagingDouble figure on a dashboard while computing the authoritative BigDecimal average for the ledger, and reserving double strictly for non-monetary metrics.

JAVA
// Follow-up: produce both a dashboard double and an authoritative BigDecimal per department.
record DeptAverage(double fast, java.math.BigDecimal authoritative) {}

Visual Overview

DIAGRAM
  List<Employee>
       │
       ├──────────────► averagingDouble(salary.doubleValue())
       │                     │  sum/count as double
       │                     ▼
       │                Map<String, Double>  (fast, lossy)
       │                1033333.3333333334
       │
       └──────────────► toList() then reduce + divide(scale, HALF_EVEN)
                             │  exact BigDecimal sum / count
                             ▼
                        Map<String, BigDecimal>  (exact)
                        1033333.33
DIAGRAM
  Rounding at a halfway value:

  value 2.5           value 3.5
  ┌───────────────┐   ┌───────────────┐
  │ HALF_UP   -> 3 │   │ HALF_UP   -> 4 │
  │ HALF_EVEN -> 2 │   │ HALF_EVEN -> 4 │   even digit chosen
  └───────────────┘   └───────────────┘

Key Takeaway

averagingDouble is a downstream collector that maps each element to a double and returns the arithmetic mean as a Double, tracking the running sum and count so it needs only one pass. Nested in groupingBy it gives the average of a numeric field per group. For metrics like average age or latency it is perfect, because a double is an acceptable representation.

The limitation to always flag is currency. double cannot represent many decimal fractions exactly, and dividing compounds that inaccuracy, so for the finance ledger Priya at the National Bank of India never averages money as a double. She shows two figures: a fast averagingDouble number for the dashboard, and an authoritative average computed by summing BigDecimal salaries exactly and dividing by the count with an explicit scale of two and HALF_EVEN rounding.

The senior point to stress is controlling scale and rounding deliberately. BigDecimal division requires a scale and a rounding mode, and HALF_EVEN, banker's rounding, is preferred because it reduces cumulative bias compared with always rounding half up, which matters when many averages roll into a total. Calling divide without a scale throws an ArithmeticException on a non-terminating result, so the scale is not optional.

The empty-group case needs a guard, since dividing by a zero count throws, though a groupingBy result never holds an empty group. The authoritative path is therefore both exact and safe: sum with BigDecimal, divide with a fixed scale and HALF_EVEN, and reserve averagingDouble for non-monetary metrics.

Remember one sentence: averagingDouble is a fast double mean fine for metrics, but money must be averaged as a BigDecimal with an explicit scale and HALF_EVEN rounding.

---

Share
Use ← → arrow keys to navigate