Definition
- Pass 1:
groupingBy(dept, averagingDouble(salary))producesMapof per-department averages. - Pass 2:
filter(e -> salary > avgMap.get(dept))keeps only above-average employees. - Two passes are mandatory because you must know all department averages before comparing individuals.
- Group the result by department for a per-department breakdown of above-average earners.
- SQL equivalent:
WHERE salary > (SELECT AVG(salary) FROM emp WHERE dept = outer.dept).
At the National Bank of India, the performance team first calculates the average pay in each department, then flags every employee earning more than their department's average for a compensation review.
Understanding the Concepts
What does finding above-average earners per department involve?
This is a correlated comparison: each employee is compared not to a global average but to the average of their own department. You cannot do this in a single stream pass because the averages must be fully computed before any comparison can happen. This makes it a two-pass problem.
How do you implement it with streams?
package com.bank.hr;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;
public class AboveDeptAverageEarners {
record Employee(String id, String name, String department, BigDecimal salary) {}
public static void main(String[] args) {
List<Employee> staff = List.of(
new Employee("SAV2001", "Rajesh", "TRADING", new BigDecimal("1800000")),
new Employee("SAV2002", "Priya", "TRADING", new BigDecimal("1200000")),
new Employee("SAV2003", "Meera", "TRADING", new BigDecimal("1500000")),
new Employee("SAV2004", "Deepak", "RISK", new BigDecimal("1500000")),
new Employee("SAV2005", "Amit", "RISK", new BigDecimal("900000")),
new Employee("SAV2006", "Suresh", "OPERATIONS", new BigDecimal("1100000")),
new Employee("SAV2007", "Kavita", "OPERATIONS", new BigDecimal("700000"))
);
// Pass 1: Compute average salary per department
Map<String, Double> avgByDept = staff.stream()
.collect(Collectors.groupingBy(
Employee::department,
Collectors.averagingDouble(e -> e.salary().doubleValue())));
// Pass 2: Filter employees earning above their department's average
Map<String, List<Employee>> aboveAvg = staff.stream()
.filter(e -> e.salary().doubleValue() > avgByDept.get(e.department()))
.collect(Collectors.groupingBy(Employee::department));
aboveAvg.forEach((dept, employees) -> {
System.out.printf("%s (avg: Rs.%.0f):%n", dept, avgByDept.get(dept));
employees.forEach(e ->
System.out.printf(" %s: Rs.%s%n", e.name(), e.salary()));
});
}
}
TRADING (avg: Rs.1500000):
Rajesh: Rs.1800000
RISK (avg: Rs.1200000):
Deepak: Rs.1500000
OPERATIONS (avg: Rs.900000):
Suresh: Rs.1100000
Why can this not be done in one pass?
The average of a department is computed from all employees in that department. If you try to compare an employee's salary to the average while still computing the average, you do not have the full picture yet. The first employee processed might be above the partial average but below the final one.
This is the same reason SQL uses a correlated subquery or a window function: the aggregate must be finalized before the filter.
How is this different from finding the top earner per department?
| Pattern | Above average | Top earner |
|---|---|---|
| What it finds | All employees above dept average | Single highest-paid per dept |
| Count of results | Zero to many per department | Exactly one per department |
| Approach | Two-pass: average then filter | Single-pass: groupingBy + maxBy |
| Collector | averagingDouble + filter | maxBy(comparing(salary)) |
What are common mistakes?
Mistake: Trying to do it in a single pass.
// WRONG: average is not known during filtering
staff.stream()
.filter(e -> e.salary().doubleValue() > /* what goes here? */)
.collect(Collectors.groupingBy(Employee::department));
// You cannot reference avgByDept because it does not exist yet
// CORRECT: two passes
Map<String, Double> avgByDept = staff.stream()
.collect(Collectors.groupingBy(Employee::department,
Collectors.averagingDouble(e -> e.salary().doubleValue())));
List<Employee> aboveAvg = staff.stream()
.filter(e -> e.salary().doubleValue() > avgByDept.get(e.department()))
.toList();
What about including employees AT the average?
Change > to >= in the filter predicate.
.filter(e -> e.salary().doubleValue() >= avgByDept.get(e.department()))
Note that with double comparisons, rounding means an employee whose salary is exactly the calculated average might be included or excluded depending on floating-point precision. For exact comparisons, use BigDecimal averages.
What about counting above-average employees per department?
Replace the downstream toList() with counting() after the filter.
Map<String, Long> countAboveAvg = staff.stream()
.filter(e -> e.salary().doubleValue() > avgByDept.get(e.department()))
.collect(Collectors.groupingBy(Employee::department, Collectors.counting()));
What are common interview follow-up questions?
- "Can this be done in one pass?" No, the averages must be fully computed before filtering.
- "How do you include those AT the average?" Change
>to>=. - "How do you count above-average employees per department?" Use
counting()as the downstream collector. - "How do you find below-average earners?" Change
>to<in the filter.
Visual Overview
Pass 1: Compute department averages ┌────────────────────────────────────────┐ │ TRADING → avg(1.8M,1.2M,1.5M) = 1.5M │ │ RISK → avg(1.5M,0.9M) = 1.2M │ │ OPERATIONS → avg(1.1M,0.7M) = 0.9M │ └────────────────────────────────────────┘ Pass 2: Filter above average ┌────────────────────────────────────────────┐ │ Rajesh TRADING 1.8M > 1.5M ✓ above │ │ Priya TRADING 1.2M > 1.5M ✗ below │ │ Meera TRADING 1.5M > 1.5M ✗ at avg │ │ Deepak RISK 1.5M > 1.2M ✓ above │ │ Amit RISK 0.9M > 1.2M ✗ below │ │ Suresh OPS 1.1M > 0.9M ✓ above │ │ Kavita OPS 0.7M > 0.9M ✗ below │ └────────────────────────────────────────────┘
Key Takeaway
Finding employees who earn more than their department's average is a two-pass problem. The first pass computes the average salary per department using groupingBy with averagingDouble. The second pass filters employees whose salary exceeds their department's average from the precomputed map.
At the National Bank of India, the performance team uses this pattern to identify above-average earners for compensation reviews. The two passes are mandatory because all department averages must be finalized before any individual comparison can happen. Trying to filter and compute the average in a single pass would compare employees against incomplete averages.
This mirrors the SQL correlated subquery pattern: WHERE salary > (SELECT AVG(salary) FROM emp WHERE dept = outer.dept). The stream version makes the dependency between the two passes explicit by computing the average map first and referencing it in the filter lambda.
Remember one sentence: two passes are mandatory because you must know all department averages before comparing individual salaries.
---