Back to Stream Practice: Employee Analysis
Q3Question 3 of 43

Average age and salary of employees by gender: overall and per department

Key Concepts
  • groupingBy(gender, averagingInt(age)) produces Map<String, Double> for average age per gender
  • For BigDecimal salary, use averagingDouble(e -> e.getSalary().doubleValue()) as the downstream
  • Triple nesting groupingBy(dept, groupingBy(gender, averagingDouble(salary))) gives per-department per-gender averages
  • summarizingInt and summarizingDouble give count, sum, min, average, and max in one pass
  • averagingInt returns 0.0 for empty groups, not NaN or an error
  • For precise money calculations, reduce with BigDecimal instead of averagingDouble to avoid floating-point drift

Definition

Use groupingBy(Employee::getGender, averagingInt(Employee::getAge)) for average age by gender, and nest groupingBy three levels deep for per-department per-gender breakdowns.
  • 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))) produces Map>.
  • summarizingInt / summarizingDouble provide count, sum, min, average, and max in a single pass as IntSummaryStatistics or DoubleSummaryStatistics.
  • 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.

JAVA
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));
    }
}
TEXT
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.

JAVA
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));
    }
}
TEXT
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.

JAVA
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));
        });
    }
}
TEXT
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.

JAVA
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()));
    }
}
TEXT
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 | ToDoubleFunction |
| 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 |

JAVA
// 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.

JAVA
// 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.

JAVA
// 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.

JAVA
// 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.

JAVA
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);
    }
}
TEXT
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

DIAGRAM
  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.

---

Share
Use ← → arrow keys to navigate