Definition
- averagingInt(Employee::getAge) computes the arithmetic mean of integer values within each group, returning
Double. - averagingDouble(e -> e.getSalary().doubleValue()) computes the mean for BigDecimal salary by converting to double first.
- Triple nesting
groupingBy(dept, groupingBy(gender, averagingDouble(salary)))producesMap.> - summarizingInt / summarizingDouble provide count, sum, min, average, and max in a single pass as
IntSummaryStatisticsorDoubleSummaryStatistics. - Precision warning: averagingDouble loses precision for large BigDecimal values; use reduce with BigDecimal.add for exact money totals.
The National Bank of India compensation committee asks: "What is the average age of male vs female employees?" That is one groupBy with an averaging downstream. Then they ask: "Break that down by department." That is a nested groupBy inside a groupBy. Finally they say: "Give me all the stats: count, sum, min, average, max." That is summarizingDouble, which is like asking the bank's reporting system for a complete statistical summary in one query instead of five separate queries.
Understanding the Concepts
What does this question ask?
The interviewer wants you to compute averages across groups. The first level groups by gender. The second level groups by department and then by gender. This tests your ability to use averaging downstream collectors and to nest them inside groupingBy.
How do you compute average age by gender?
Use Collectors.groupingBy with Employee::getGender as the classifier and Collectors.averagingInt(Employee::getAge) as the downstream.
package com.bank.hr;
import java.util.*;
import java.util.stream.Collectors;
public class AvgAgeByGender {
public static void main(String[] args) {
List<Employee> employees = Employee.getSampleEmployees();
Map<String, Double> avgAgeByGender = employees.stream()
.collect(Collectors.groupingBy(
Employee::getGender,
Collectors.averagingInt(Employee::getAge)));
avgAgeByGender.forEach((gender, avg) ->
System.out.printf("%s: average age = %.1f%n", gender, avg));
}
}
M: average age = 49.6
F: average age = 36.6
The result type is Map. The averagingInt collector converts each employee's age to an int, sums them, and divides by the count.
How do you compute average salary by gender using BigDecimal?
Since salary is BigDecimal, you must convert to double for the averaging collector.
package com.bank.hr;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;
public class AvgSalaryByGender {
public static void main(String[] args) {
List<Employee> employees = Employee.getSampleEmployees();
Map<String, Double> avgSalaryByGender = employees.stream()
.collect(Collectors.groupingBy(
Employee::getGender,
Collectors.averagingDouble(
e -> e.getSalary().doubleValue())));
avgSalaryByGender.forEach((gender, avg) ->
System.out.printf("%s: average salary = Rs.%.2f%n", gender, avg));
}
}
M: average salary = Rs.1440000.00
F: average salary = Rs.1000000.00
The conversion e.getSalary().doubleValue() loses precision for very large values. For production code with exact money calculations, use a reduce-based approach instead.
How do you compute per-department per-gender average?
Nest groupingBy three levels deep: outer = department, middle = gender, innermost = averaging.
package com.bank.hr;
import java.util.*;
import java.util.stream.Collectors;
public class AvgAgePerDeptGender {
public static void main(String[] args) {
List<Employee> employees = Employee.getSampleEmployees();
Map<String, Map<String, Double>> avgAgePerDeptGender = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.groupingBy(
Employee::getGender,
Collectors.averagingInt(Employee::getAge))));
avgAgePerDeptGender.forEach((dept, genderMap) -> {
System.out.println(dept + ":");
genderMap.forEach((gender, avg) ->
System.out.printf(" %s: avg age = %.1f%n", gender, avg));
});
}
}
TRADING:
M: avg age = 46.5
F: avg age = 28.0
RISK:
M: avg age = 55.0
F: avg age = 33.0
OPERATIONS:
M: avg age = 58.0
F: avg age = 42.0
How does summarizingInt give all statistics in one pass?
Collectors.summarizingInt and summarizingDouble return a statistics object with count, sum, min, average, and max. This is more efficient than computing each statistic separately.
package com.bank.hr;
import java.util.*;
import java.util.stream.Collectors;
public class SummarizingDemo {
public static void main(String[] args) {
List<Employee> employees = Employee.getSampleEmployees();
Map<String, IntSummaryStatistics> statsByGender = employees.stream()
.collect(Collectors.groupingBy(
Employee::getGender,
Collectors.summarizingInt(Employee::getAge)));
statsByGender.forEach((gender, stats) ->
System.out.printf("%s: count=%d, min=%d, avg=%.1f, max=%d, sum=%d%n",
gender, stats.getCount(), stats.getMin(),
stats.getAverage(), stats.getMax(), stats.getSum()));
}
}
M: count=5, min=35, avg=49.6, max=58, sum=248
F: count=5, min=28, avg=36.6, max=45, sum=183
One pass through the stream produces five metrics. In the old for-loop approach, you would maintain separate count, sum, min, and max variables.
How is averagingInt different from averagingDouble?
| Feature | averagingInt | averagingDouble |
|---|---|---|
| Input | ToIntFunction
| Internal accumulation | long sum + long count | double sum + long count |
| Result | Double | Double |
| Precision | Exact for int sums up to Long.MAX_VALUE | Subject to floating-point rounding |
| Use for salary (BigDecimal) | No (BigDecimal has no intValue for large values) | Yes (via doubleValue()) |
| Use for age (int) | Yes | Also works but unnecessary |
// averagingInt for age (int field)
Double avgAge = employees.stream()
.collect(Collectors.averagingInt(Employee::getAge));
// averagingDouble for salary (BigDecimal field)
Double avgSalary = employees.stream()
.collect(Collectors.averagingDouble(
e -> e.getSalary().doubleValue()));
What are common mistakes?
Mistake 1: using averagingDouble directly on BigDecimal without conversion.
// WRONG: BigDecimal is not a number primitive
Double wrong = employees.stream()
.collect(Collectors.averagingDouble(Employee::getSalary)); // compile error
// CORRECT: convert to double
Double correct = employees.stream()
.collect(Collectors.averagingDouble(
e -> e.getSalary().doubleValue()));
Mistake 2: expecting averagingInt to return int.
// WRONG: averagingInt returns Double, not int
int wrong = employees.stream()
.collect(Collectors.averagingInt(Employee::getAge)); // compile error
// CORRECT: store as Double
Double correct = employees.stream()
.collect(Collectors.averagingInt(Employee::getAge));
Mistake 3: losing precision with BigDecimal salary averaging.
// WRONG for exact money: averagingDouble introduces floating-point error
Double approximate = employees.stream()
.collect(Collectors.averagingDouble(
e -> e.getSalary().doubleValue()));
// Result: 1220000.0000000002 (floating-point drift)
// CORRECT for exact money: reduce with BigDecimal
BigDecimal totalSalary = employees.stream()
.map(Employee::getSalary)
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal exactAvg = totalSalary.divide(
BigDecimal.valueOf(employees.size()),
2, java.math.RoundingMode.HALF_UP);
// Result: 1220000.00 (exact)
What about computing pay gap percentage?
A common follow-up is to compute the gender pay gap from the grouped averages.
package com.bank.hr;
import java.util.*;
import java.util.stream.Collectors;
public class PayGapCalculator {
public static void main(String[] args) {
List<Employee> employees = Employee.getSampleEmployees();
Map<String, Double> avgSalaryByGender = employees.stream()
.collect(Collectors.groupingBy(
Employee::getGender,
Collectors.averagingDouble(
e -> e.getSalary().doubleValue())));
double maleAvg = avgSalaryByGender.getOrDefault("M", 0.0);
double femaleAvg = avgSalaryByGender.getOrDefault("F", 0.0);
double gapPercent = ((maleAvg - femaleAvg) / maleAvg) * 100;
System.out.printf("Male avg: Rs.%.2f%n", maleAvg);
System.out.printf("Female avg: Rs.%.2f%n", femaleAvg);
System.out.printf("Pay gap: %.1f%%%n", gapPercent);
}
}
Male avg: Rs.1440000.00
Female avg: Rs.1000000.00
Pay gap: 30.6%
What are common interview follow-up questions?
- "How do you find the department with the highest average salary?" Stream the per-department average map and use max(comparingByValue()).
- "What if some employees have null salary?" Filter out nulls before averaging, or use Objects.requireNonNullElse with a default.
- "How do you compute median instead of average?" Collect to a sorted list per group, then pick the middle element. There is no built-in median collector.
- "How do you round the average to 2 decimal places?" Use Math.round(avg * 100.0) / 100.0 or BigDecimal.valueOf(avg).setScale(2, RoundingMode.HALF_UP).
Visual Overview
Stream<Employee>
│
│ .collect(groupingBy(gender, averagingInt(age)))
▼
┌─────────────────────────────────┐
│ Map<String, Double> │
│ │
│ "M" ──→ 49.6 │
│ "F" ──→ 36.6 │
└─────────────────────────────────┘
Triple nesting:
groupingBy(department) ← outer key
│
├── groupingBy(gender) ← inner key
│ │
│ └── averagingInt(age) ← leaf value
▼
Map<String, Map<String, Double>>
TRADING ──→ { M: 46.5, F: 28.0 }
RISK ─────→ { M: 55.0, F: 33.0 }
Key Takeaway
Computing averages by gender uses groupingBy with averagingInt or averagingDouble as the downstream collector. For age: groupingBy(Employee::getGender, averagingInt(Employee::getAge)) produces Map. For BigDecimal salary: convert to double first with averagingDouble(e -> e.getSalary().doubleValue()).
Per-department per-gender averages use triple nesting: groupingBy(department, groupingBy(gender, averagingDouble(salary))). The result is Map which reads as: for each department, a map from gender to average salary.
For complete statistics in one pass, summarizingInt and summarizingDouble return count, sum, min, average, and max together. This replaces the manual approach of maintaining separate counters and totals in for-loops.
For exact money calculations with BigDecimal, avoid averagingDouble because of floating-point drift. Instead, reduce with BigDecimal::add to get the exact total, then divide by count using RoundingMode.HALF_UP.
Remember one sentence: averagingInt and averagingDouble are downstream collectors that compute the arithmetic mean within each group, and summarizing gives you count, sum, min, avg, and max in one pass.
---