Definition
maxBy(comparator)reduces each group to its maximum element, returningOptional.collectingAndThen(collector, finisher)runs a collector then applies a finishing function.Optional::getunwraps the Optional; safe because groupingBy guarantees non-empty groups.- Tie-breaking: maxBy keeps the last maximum; add
thenComparingfor determinism. - Extends to:
minByfor 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?
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()));
}
}
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
What about the lowest earner per department?
Replace maxBy with minBy.
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.
// 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.
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?
// 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?
collectingAndThenis a general-purpose adapter: any collector can have a finisher applied.Optional::getis 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
thenComparingtie-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
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
---