Back to Stream: Grouping & Partitioning
Q13Question 13 of 22

Group employees by department, then find the second-highest salary in each department

Key Concepts
  • Group by department, sort each group descending, skip(1).findFirst() to get the runner-up
  • collectingAndThen(toList(), finisher) post-processes each group after collection
  • The result is Optional<Employee> per department because a single-person department has no second-highest
  • Ties must be handled deliberately: two equal top salaries may both be 'first', making skip(1) the actual second person
  • For second distinct salary, extract distinct salaries first, then find the second
  • For large groups, a bounded priority structure avoids full sorting, but sort-and-skip is clear for typical sizes

Definition

Group by department, sort each group by salary descending, skip(1).findFirst() to get the runner-up employee.
  • 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 Map> because 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.

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

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

JAVA
// 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():

JAVA
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

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

---

Share
Use ← → arrow keys to navigate