Definition
- groupingBy(dept, collectingAndThen(toList(), finisher)) groups and post-processes.
- The finisher sorts descending by salary, then skip(1).findFirst() skips the top earner and returns the next.
- Result is
Mapbecause a single-person department has no second-highest.> - Ties: if two people share the top salary, skip(1) returns the second person, not the second distinct salary.
For each department at the National Bank of India, Priya wants the runner-up, not the winner. She lines each department up by pay, ignores the number one, and picks the next person.
Understanding the Concepts
How do you find the second-highest paid per department?
Group by department, then post-process each group with collectingAndThen. The finisher sorts descending by salary, then skip(1).findFirst() skips the top earner.
package com.bank.hr;
import java.util.*;
import java.util.stream.Collectors;
public class SecondHighestPerDept {
public static void main(String[] args) {
List<Employee> employees = List.of(
new Employee("Rajesh", "Loans", 75000.0),
new Employee("Priya", "Loans", 82000.0),
new Employee("Amit", "Loans", 68000.0),
new Employee("Kavita", "IT", 91000.0),
new Employee("Neha", "IT", 78000.0),
new Employee("Suresh", "Finance", 70000.0)
);
Map<String, Optional<Employee>> secondHighest = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.collectingAndThen(
Collectors.toList(),
list -> {
list.sort(Comparator.comparingDouble(
Employee::getSalaryAsDouble).reversed());
return list.stream().skip(1).findFirst();
})));
secondHighest.forEach((dept, opt) ->
opt.ifPresentOrElse(
e -> System.out.println(dept + ": " + e.getName()
+ " (" + e.getSalaryAsDouble() + ")"),
() -> System.out.println(dept + ": No runner-up")));
}
}
Loans: Rajesh (75000.0)
IT: Neha (78000.0)
Finance: No runner-up
Why is the result Optional?
A department with only one employee has no second-highest. skip(1) on a single-element stream produces an empty stream, and findFirst() returns Optional.empty(). This correctly represents "there is no runner-up" without throwing.
How do you handle ties?
If two employees share the top salary, "second-highest" is ambiguous. Decide which meaning you want:
// Meaning 1: second person (skip the first person regardless of ties)
// This is what the basic approach does.
// Meaning 2: second distinct salary level
list.sort(Comparator.comparingDouble(
Employee::getSalaryAsDouble).reversed());
double topSalary = list.get(0).getSalaryAsDouble();
return list.stream()
.filter(e -> e.getSalaryAsDouble() < topSalary)
.findFirst();
What are common mistakes?
Mistake: forgetting to handle the Optional for single-person departments.
// WRONG: get() throws NoSuchElementException if department has 1 person
Employee wrong = secondHighest.get("Finance").get(); // throws!
// CORRECT: handle the empty case
secondHighest.get("Finance").ifPresent(e ->
System.out.println(e.getName()));
How do you find the Nth-highest per department?
Generalise with skip(n-1).findFirst():
int n = 3; // third-highest
return list.stream().skip(n - 1).findFirst();
What should a senior engineer know?
- For small department sizes, sort-and-skip is clear and fast enough. For very large groups, use a bounded priority queue to keep only the top-N without sorting the full list.
- The finisher runs per group, so sorting cost is proportional to each group's size, not the total.
- Be explicit about tie-breaking: extend the comparator with a secondary key for deterministic results.
- This is a classic interview question that tests groupingBy, collectingAndThen, and Optional handling.
What are common interview follow-up questions?
- "How do you find the Nth-highest across all departments?" sorted(desc).skip(n-1).findFirst() on the full stream.
- "How do you find the second-highest distinct salary?" Filter out the maximum before taking max again.
- "What if two departments have the same runner-up salary?" They are independent; each department reports its own runner-up.
Visual Overview
Loans group: [Priya:82K, Rajesh:75K, Amit:68K] (sorted desc)
│
│ skip(1)
▼
[Rajesh:75K, Amit:68K]
│
│ findFirst()
▼
Optional[Rajesh:75K] ← second-highest
Finance group: [Suresh:70K] (only one person)
│
│ skip(1)
▼
[] (empty)
│
│ findFirst()
▼
Optional.empty ← no runner-up
Key Takeaway
To find the second-highest earner in each department, group by department and post-process with collectingAndThen. The finishing function sorts descending by salary, skips the first element (the top earner), and takes findFirst() to get the runner-up as an Optional.
The Optional is important: a department with only one person has no second-highest, so skip(1) produces an empty stream and findFirst() correctly returns empty. At the National Bank of India, this drove a succession planning dashboard that showed the likely deputy per department.
Be deliberate about ties. If two people share the top salary, skip(1) returns the second person, not the second distinct salary. If you want the second distinct salary, filter out entries matching the top salary before taking findFirst.
Remember one sentence: sort each group descending, skip(1).findFirst() gives the runner-up as Optional, and single-person departments correctly return empty.
---