Back to Stream: Grouping & Partitioning
Q4Question 4 of 22

Group employees by department, then get the employee with the highest salary in each department

Key Concepts
  • maxBy(Comparator.comparing(getSalary)) finds the highest-salary employee per group, returning Optional
  • collectingAndThen(maxBy(...), Optional::get) unwraps the Optional for a clean Map<String, Employee>
  • Optional::get is safe here because groupingBy never creates a key for an empty group
  • If keys came from a fixed list, a group could be empty and get would throw: use orElse(null) instead
  • For ties in salary, maxBy keeps the last maximum; add thenComparing for deterministic tie-breaking
  • The pattern extends to minBy for the lowest earner per department

Definition

`groupingBy(dept, collectingAndThen(maxBy(by salary), Optional::get))` gives the top earner per department.
  • maxBy(comparator) reduces each group to its maximum element, returning Optional.
  • collectingAndThen(collector, finisher) runs a collector then applies a finishing function.
  • Optional::get unwraps the Optional; safe because groupingBy guarantees non-empty groups.
  • Tie-breaking: maxBy keeps the last maximum; add thenComparing for determinism.
  • Extends to: minBy for the lowest earner per department.

Priya wants the top earner in each department: the department champion. For every department tray she looks through the cards and keeps the one with the biggest salary.

Understanding the Concepts

How do you find the top earner per department?

JAVA
package com.bank.hr;

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

public class TopEarnerPerDept {
    public static void main(String[] args) {
        List<Employee> staff = List.of(
            new Employee("Rajesh",  "Loans",   new BigDecimal("95000")),
            new Employee("Kavita",  "Loans",   new BigDecimal("78000")),
            new Employee("Priya",   "IT",      new BigDecimal("120000")),
            new Employee("Suresh",  "IT",      new BigDecimal("110000")),
            new Employee("Amit",    "Finance", new BigDecimal("85000")));

        Map<String, Employee> topEarners = staff.stream()
            .collect(Collectors.groupingBy(
                Employee::getDepartment,
                Collectors.collectingAndThen(
                    Collectors.maxBy(Comparator.comparing(Employee::getSalary)),
                    Optional::get)));

        topEarners.forEach((dept, e) -> System.out.printf("%s: %s (Rs. %s)%n",
            dept, e.getName(), e.getSalary().toPlainString()));
    }
}
TEXT
Loans: Rajesh (Rs. 95000)
IT: Priya (Rs. 120000)
Finance: Amit (Rs. 85000)

Why is Optional::get safe here?

groupingBy only creates a key when at least one element falls into that group. Since a group always has at least one member, the maxBy Optional is always present, and get never throws.

When would Optional::get be unsafe?

If the keys came from a fixed list of all possible departments and some departments had no employees, the Optional would be empty and get would throw. In that case, use orElse(null) or filter.

How does collectingAndThen work?

| Step | What happens |
|---|---|
| 1. Run downstream | maxBy finds the maximum, producing Optional |
| 2. Apply finisher | Optional::get unwraps to Employee |
| Result | Map instead of Map> |

What about the lowest earner per department?

Replace maxBy with minBy.

JAVA
Map<String, Employee> lowestEarners = staff.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.collectingAndThen(
            Collectors.minBy(Comparator.comparing(Employee::getSalary)),
            Optional::get)));

What are common mistakes?

Mistake: not adding a tie-breaking comparator when two employees have the same salary.

JAVA
// Potential tie: maxBy keeps the last seen, which may vary
// CORRECT: add thenComparing for deterministic tie-break
Comparator<Employee> comp = Comparator.comparing(Employee::getSalary)
    .thenComparing(Employee::getName);

Map<String, Employee> deterministic = staff.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.collectingAndThen(
            Collectors.maxBy(comp),
            Optional::get)));

What about keeping the Optional wrapper?

Without collectingAndThen, the result is Map>.

JAVA
Map<String, Optional<Employee>> withOptional = staff.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.maxBy(Comparator.comparing(Employee::getSalary))));
// Values are Optional<Employee>; caller must unwrap

What about using reducing instead of maxBy?

JAVA
// Equivalent with reducing
Map<String, Optional<Employee>> withReducing = staff.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.reducing((a, b) ->
            a.getSalary().compareTo(b.getSalary()) >= 0 ? a : b)));

What should a senior engineer know?

  • collectingAndThen is a general-purpose adapter: any collector can have a finisher applied.
  • Optional::get is safe only under the groupingBy guarantee of non-empty groups.
  • For production code, add a deterministic tie-breaking comparator.
  • The same pattern works with minBy, reducing, and other Optional-returning downstreams.

What are common interview follow-up questions?

  • "What if two employees have the same salary?" Add a thenComparing tie-breaker.
  • "What does collectingAndThen do?" Runs a collector then applies a finishing function to the result.
  • "Why not just use maxBy?" You can, but the values are wrapped in Optional, which is awkward to consume.

Visual Overview

DIAGRAM
  groupingBy(dept, collectingAndThen(maxBy(salary), Optional::get))

  Loans:   [Rajesh(95000), Kavita(78000)] → max → Rajesh
  IT:      [Priya(120000), Suresh(110000)] → max → Priya
  Finance: [Amit(85000)]                   → max → Amit

  Map<String, Employee>
  ┌──────────────────────────────────┐
  │  Loans   → Rajesh (95000)        │
  │  IT      → Priya  (120000)       │
  │  Finance → Amit   (85000)        │
  └──────────────────────────────────┘

Key Takeaway

To find the top earner per department I group by department with maxBy(Comparator.comparing(Employee::getSalary)) as the downstream. Since maxBy returns Optional, I wrap it in collectingAndThen with Optional::get as the finisher to produce a clean Map of department to Employee.

Optional::get is safe here because groupingBy never creates a key for an empty group. If keys came from a fixed list of all departments, some might be empty and get would throw, so there I would use orElse(null).

In my banking project at the National Bank of India this produced the quarterly top-performer-by-department list. I always add a thenComparing tie-breaker for deterministic results when two salaries are equal.

Remember one sentence: collectingAndThen(maxBy(salary), Optional::get) unwraps the per-group maximum into a clean Map.

---

Share
Use ← → arrow keys to navigate