Hook: Interviewers love this one because it looks tiny, but it checks whether you notice duplicates, ordering, and empty results.
Question: How do you find the second highest salary using Streams in Java?
Answer: Turn the salaries into a stream, remove duplicate values, sort them from high to low, and take the second item. If there are fewer than two distinct salaries, return an empty result instead of forcing a value. In Streams, that usually means using map, distinct, sorted, skip, and findFirst.
Interview-Ready Answer: I’d stream the employees, map each one to salary, remove duplicate salaries with distinct(), sort descending, then skip the highest and read the next value. I’d return an Optional so the method stays safe when there are fewer than two distinct salaries. That gives the correct second highest distinct salary and avoids the common duplicate-value bug.
Detailed Explanation: In interview language, second highest salary usually means the second highest distinct salary. If two employees both earn 120000, that still counts as one top salary, not two positions. That small word, distinct, is the whole trap.
employees.stream(). A stream is just a pipeline for processing data in stages.map(Employee::getSalary) to extract only salaries. This changes each employee object into a salary value.distinct() to remove duplicates. Stateful means it remembers what it has already seen, so repeated salaries are ignored.sorted(Comparator.reverseOrder()) to put salaries in descending order, highest first.skip(1) to move past the highest salary.findFirst() to take the next one. The result is an Optional because there may not be a second distinct salary.One important detail: sorted() is a terminal-heavy step in practice because it must collect and order all elements before it can hand out results. That is why this solution is clean, but not the most memory-friendly option for huge lists.
Use this stream style when readability matters and the dataset is moderate, such as a few hundred or a few thousand rows. It is great in interview code because it shows you know the Stream API and can handle duplicates correctly. If the dataset is very large and performance is critical, a one-pass loop is faster because it avoids sorting.
| Approach | Idea | Time | Space | Good for |
|---|---|---|---|---|
| Stream pipeline | Distinct, sort, skip | O(n log n) | O(n) | Readable interview code |
| TreeSet | Keep unique sorted values | O(n log n) | O(n) | Neat alternative |
| One-pass loop | Track top two | O(n) | O(1) | Best performance |
Performance note: Sorting 1 million salaries means a lot of comparisons and extra memory pressure. The stream version is still correct, but it is not the fastest possible answer. In a real system, you would choose the simplest correct version that fits the data size.
distinct().int, use long or BigDecimal.Think of it like a podium: first remove duplicate gold medals, then take the silver medal. That mental picture keeps you from accidentally returning the same top salary twice.
Real-World Story: Imagine a payroll dashboard in a fintech company that shows the top salaries by department. The report runs every night and feeds an executive summary email.
The team once wrote a stream pipeline without distinct(). In the sales department, two senior managers had the same highest salary, so the dashboard showed the second highest salary as the same number as the highest. Nobody noticed at first because the result looked believable.
What went wrong: the compensation review page started showing duplicate top values, and HR questioned whether the data was corrupted. The symptom was subtle: no crash, just a wrong number in a report. Logs were clean, but users saw a suspicious ranking that made audits and bonus planning harder. The fix was to treat the problem as a distinct salary question, not a simple sorted-list question.
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
public class Main {
static final class Employee {
private final String name;
private final Integer salary;
Employee(String name, Integer salary) {
this.name = name;
this.salary = salary;
}
String getName() {
return name;
}
Integer getSalary() {
return salary;
}
@Override
public String toString() {
return name + ":" + salary;
}
}
// We return Optional<Integer> because there may be fewer than two distinct salaries.
static Optional<Integer> secondHighestSalary(List<Employee> employees) {
Objects.requireNonNull(employees, "employees must not be null");
return employees.stream()
.filter(Objects::nonNull) // Ignore null employee records instead of failing mid-stream.
.map(Employee::getSalary)
.filter(Objects::nonNull) // Null salary values are not valid for ranking.
.distinct() // Critical: second highest means second DISTINCT salary.
.sorted(Comparator.reverseOrder())
.skip(1) // Skip the highest distinct salary.
.findFirst(); // May be empty if only one distinct salary exists.
}
public static void main(String[] args) {
List<Employee> team = List.of(
new Employee("Asha", 120000),
new Employee("Ben", 95000),
new Employee("Cara", 120000),
new Employee("Dev", 110000),
new Employee("Eli", 95000)
);
System.out.println("Team: " + team);
System.out.println("Second highest distinct salary: " + secondHighestSalary(team).orElse(null));
// Edge case: all salaries are the same, so there is no second distinct salary.
List<Employee> sameSalaryTeam = List.of(
new Employee("Nina", 50000),
new Employee("Omar", 50000)
);
Optional<Integer> result = secondHighestSalary(sameSalaryTeam);
System.out.println("All same salaries present? " + result.isPresent());
System.out.println("Fallback value if missing: " + result.orElse(-1));
// Edge case: empty list also returns empty rather than throwing or guessing.
System.out.println("Empty list result: " + secondHighestSalary(List.of()).orElse(null));
}
}
Follow-up & Tricky Questions:
groupingBy plus a downstream ranking step.Optional instead of null? Optional makes the absence of a second salary explicit and safer. It tells the caller, very clearly, that the result may not exist.BigDecimal? You can still stream them, but compare with compareTo or a proper comparator. Never use subtraction for money values because precision matters.max twice? Yes, but only if you first remove the highest salary from the data set or keep two running maxima. Otherwise, duplicates can make the second result equal the first.findFirst() always deterministic? On this ordered pipeline it is, because sorted() establishes order. If you switch to findAny() or lose ordering, the result can vary.distinct() if the question means second highest distinct salary.Common Mistakes:
distinct(): then duplicate top salaries can make the answer wrong. Correction: remove duplicates before ranking.Integer as a forced answer: this hides missing data. Correction: use Optional or clearly documented fallback behavior.Memory Hook: Picture a winners’ podium: first remove duplicate gold medals, then take the silver medal. That is the whole mental model for second highest distinct salary.
Cheat Sheet:
map employees to salary values.distinct removes duplicate salaries.sorted(Comparator.reverseOrder()) puts highest first.skip(1) ignores the top salary.findFirst() returns an Optional.O(n log n); space: O(n).Practice Tasks:
List<Integer> instead of employees.