Back to Stream Practice: Employee Analysis
Q4Question 4 of 43

Find oldest, youngest female, longest-serving employee: overall and per department

Key Concepts
  • Stream.max(Comparator.comparingInt(Employee::getAge)) returns Optional<Employee> for the oldest employee
  • Chain filter(female) before min(comparingInt(age)) to find the youngest female specifically
  • groupingBy(department, maxBy(comparingInt(age))) returns Map<String, Optional<Employee>> for per-department oldest
  • collectingAndThen unwraps the Optional in map values: collectingAndThen(maxBy(...), Optional::orElseThrow)
  • max and min on an empty stream return Optional.empty(), so always handle the empty case
  • Comparator.comparing(Employee::getSalary) works for Comparable fields like BigDecimal without explicit conversion

Definition

Use Stream.max(comparingInt(Employee::getAge)) for the oldest overall, filter(female).min(comparingInt(age)) for the youngest female, and groupingBy(department, maxBy(comparingInt(age))) for per-department extremes.
  • Stream.max(Comparator) returns Optional containing the maximum element, or empty if the stream is empty.
  • Stream.min(Comparator) returns Optional containing 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 Map instead of Map>.

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?

JAVA
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() + ")"));
    }
}
TEXT
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.

JAVA
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()));
    }
}
TEXT
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?

JAVA
Optional<Employee> longestServing = employees.stream()
    .max(Comparator.comparingInt(Employee::getYearsOfService));

longestServing.ifPresent(e ->
    System.out.println("Longest serving: " + e.getName()
        + ", " + e.getYearsOfService() + " years"));
TEXT
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.

JAVA
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())));
    }
}
TEXT
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.

JAVA
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));
    }
}
TEXT
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 | Collector that produces Optional |
| Scope | Entire stream | Within each group |
| Example | employees.stream().max(comp) | groupingBy(dept, maxBy(comp)) |

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

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

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

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

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

---

Share
Use ← → arrow keys to navigate