Definition
- Stream.max(Comparator) returns
Optionalcontaining the maximum element, or empty if the stream is empty. - Stream.min(Comparator) returns
Optionalcontaining the minimum element. - filter before min/max narrows the stream to a subset (such as female employees) before finding the extreme.
- Collectors.maxBy / minBy are downstream collectors used inside groupingBy to find extremes per group.
- collectingAndThen(maxBy(...), Optional::orElseThrow) unwraps the Optional in the result map, producing
Mapinstead ofMap.>
The National Bank of India succession planning committee needs three answers. "Who is our oldest employee?" is like finding the tallest person in a room: scan everyone, remember the tallest so far. "Who is our youngest female employee?" is the same scan, but first ask all males to step out. "Who is the oldest in each department?" is like scanning each department's room separately and reporting the oldest from each one.
Understanding the Concepts
What does this question ask?
The interviewer wants you to find extreme values (max/min) in a stream, both overall and per group. This tests your understanding of Stream.max, Stream.min, Comparator.comparingInt, and the downstream collectors maxBy and minBy.
How do you find the oldest employee overall?
package com.bank.hr;
import java.util.*;
public class OldestEmployee {
public static void main(String[] args) {
List<Employee> employees = Employee.getSampleEmployees();
Optional<Employee> oldest = employees.stream()
.max(Comparator.comparingInt(Employee::getAge));
oldest.ifPresent(e ->
System.out.println("Oldest: " + e.getName()
+ ", age " + e.getAge()
+ " (SAV" + e.getAccountId() + ")"));
}
}
Oldest: Suresh, age 58 (SAV2007)
max(comparingInt(Employee::getAge)) returns Optional because the stream could be empty. The Comparator compares employees by their age field.
How do you find the youngest female employee?
Chain filter before min to narrow the stream to female employees first.
package com.bank.hr;
import java.util.*;
public class YoungestFemale {
public static void main(String[] args) {
List<Employee> employees = Employee.getSampleEmployees();
Optional<Employee> youngestFemale = employees.stream()
.filter(e -> "F".equals(e.getGender()))
.min(Comparator.comparingInt(Employee::getAge));
youngestFemale.ifPresent(e ->
System.out.println("Youngest female: " + e.getName()
+ ", age " + e.getAge()));
}
}
Youngest female: Priya, age 28
The filter runs first, keeping only female employees. The min then finds the youngest among them.
How do you find the longest-serving employee?
Optional<Employee> longestServing = employees.stream()
.max(Comparator.comparingInt(Employee::getYearsOfService));
longestServing.ifPresent(e ->
System.out.println("Longest serving: " + e.getName()
+ ", " + e.getYearsOfService() + " years"));
Longest serving: Suresh, 30 years
The same pattern works for any numeric field. Change the comparator to target the field you want.
How do you find the oldest per department?
Use Collectors.groupingBy with Collectors.maxBy as the downstream collector.
package com.bank.hr;
import java.util.*;
import java.util.stream.Collectors;
public class OldestPerDepartment {
public static void main(String[] args) {
List<Employee> employees = Employee.getSampleEmployees();
// With Optional in the values
Map<String, Optional<Employee>> oldestPerDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.maxBy(
Comparator.comparingInt(Employee::getAge))));
oldestPerDept.forEach((dept, opt) ->
opt.ifPresent(e ->
System.out.println(dept + ": " + e.getName()
+ ", age " + e.getAge())));
}
}
TRADING: Rajesh, age 45
RISK: Deepak, age 55
OPERATIONS: Suresh, age 58
COMPLIANCE: Vikram, age 52
The result type is Map because maxBy returns Optional.
How do you unwrap Optional from the map values?
Use Collectors.collectingAndThen to transform the Optional into a plain Employee.
package com.bank.hr;
import java.util.*;
import java.util.stream.Collectors;
public class UnwrapOptionalDemo {
public static void main(String[] args) {
List<Employee> employees = Employee.getSampleEmployees();
// Unwrap Optional using collectingAndThen
Map<String, Employee> oldestPerDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.collectingAndThen(
Collectors.maxBy(
Comparator.comparingInt(Employee::getAge)),
Optional::orElseThrow)));
// Alternative: map to name directly
Map<String, String> oldestNames = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.collectingAndThen(
Collectors.maxBy(
Comparator.comparingInt(Employee::getAge)),
opt -> opt.map(Employee::getName).orElse("N/A"))));
oldestNames.forEach((dept, name) ->
System.out.println(dept + ": " + name));
}
}
TRADING: Rajesh
RISK: Deepak
OPERATIONS: Suresh
COMPLIANCE: Vikram
collectingAndThen takes the result of maxBy (which is Optional) and applies a finisher function. Using Optional::orElseThrow is safe here because groupingBy guarantees each group has at least one element.
How is max/min different from maxBy/minBy?
| Feature | Stream.max / Stream.min | Collectors.maxBy / Collectors.minBy |
|---|---|---|
| Where used | Terminal operation on Stream | Downstream collector inside groupingBy |
| Returns | Optional
| Scope | Entire stream | Within each group |
| Example | employees.stream().max(comp) | groupingBy(dept, maxBy(comp)) |
// Stream.max: finds max in the entire stream
Optional<Employee> overall = employees.stream()
.max(Comparator.comparingInt(Employee::getAge));
// Collectors.maxBy: finds max within each group
Map<String, Optional<Employee>> perGroup = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.maxBy(Comparator.comparingInt(Employee::getAge))));
What are common mistakes?
Mistake 1: forgetting that max/min return Optional.
// WRONG: direct get() without checking
Employee oldest = employees.stream()
.max(Comparator.comparingInt(Employee::getAge))
.get(); // NoSuchElementException if stream is empty!
// CORRECT: use orElse or ifPresent
Employee oldest = employees.stream()
.max(Comparator.comparingInt(Employee::getAge))
.orElseThrow(() -> new IllegalStateException("No employees found"));
Mistake 2: using wrong comparator direction for youngest.
// WRONG: max finds the OLDEST, not youngest
Optional<Employee> wrong = employees.stream()
.filter(e -> "F".equals(e.getGender()))
.max(Comparator.comparingInt(Employee::getAge)); // oldest female!
// CORRECT: min finds the youngest
Optional<Employee> correct = employees.stream()
.filter(e -> "F".equals(e.getGender()))
.min(Comparator.comparingInt(Employee::getAge)); // youngest female
Mistake 3: confusing multiple tie-breaking behavior.
// If two employees have the same max age, max returns
// the first one encountered (in encounter order).
// To break ties explicitly, chain thenComparing:
Optional<Employee> tieBreaker = employees.stream()
.max(Comparator.comparingInt(Employee::getAge)
.thenComparing(Employee::getName)); // alphabetical tie-breaker
What are common interview follow-up questions?
- "What if two employees share the same max age?" Stream.max returns the first one in encounter order. Use thenComparing for explicit tie-breaking.
- "How do you find the top N oldest employees?" Use sorted(comparingInt(age).reversed()).limit(n).
- "How do you find the youngest in each city instead of department?" Change the classifier: groupingBy(Employee::getCity, minBy(comparingInt(age))).
- "How do you return the result as a sorted list of names?" Chain map(Employee::getName).collect(toList()) after sorting.
Visual Overview
Finding extremes:
Stream<Employee>
│
├── max(comparingInt(age)) ──→ Optional<Employee> (oldest)
│
├── filter(female)
│ │
│ └── min(comparingInt(age)) ──→ Optional<Employee> (youngest F)
│
└── max(comparingInt(yearsOfService)) ──→ Optional<Employee> (longest)
Per-department:
Stream<Employee>
│
│ .collect(groupingBy(dept, maxBy(comparingInt(age))))
▼
┌──────────────────────────────────────────┐
│ Map<String, Optional<Employee>> │
│ │
│ TRADING ───→ Optional[Rajesh, 45] │
│ RISK ──────→ Optional[Deepak, 55] │
│ OPERATIONS → Optional[Suresh, 58] │
└──────────────────────────────────────────┘
Key Takeaway
Finding extremes in streams uses max and min with a Comparator. The oldest employee is stream().max(comparingInt(Employee::getAge)), which returns Optional because the stream might be empty. The youngest female chains filter first: filter(e -> "F".equals(e.getGender())).min(comparingInt(Employee::getAge)). The longest-serving employee uses the same pattern with yearsOfService.
Per-department extremes use groupingBy with maxBy or minBy as the downstream: groupingBy(department, maxBy(comparingInt(age))) produces Map. The Optional in the value is because maxBy is defined to return Optional. To unwrap it, use collectingAndThen(maxBy(comparator), Optional::orElseThrow) which is safe because groupingBy guarantees at least one element per group.
The most common mistake is forgetting that max and min return Optional. Calling .get() directly without checking causes NoSuchElementException on empty streams. Always use orElse, orElseThrow, or ifPresent.
Remember one sentence: max and min return Optional, maxBy and minBy are their downstream-collector equivalents for use inside groupingBy, and collectingAndThen unwraps the Optional from the result map.
---