Definition
- groupingBy(getBirthDate().getMonth()) produces a
Mapbucketed by the> Monthenum. - Filter size > 1 keeps only months where at least two employees share the birthday month.
- flatMap flattens the matching groups into a single list of employees who share a month.
- TreeMap supplier or sort by
Month.ordinal()gives chronological output instead of hash order. - Null guard with
filter(e -> e.getBirthDate() != null)preventsNullPointerExceptionbefore grouping.
The National Bank of India HR team wants to throw one birthday celebration per month for employees sharing that month. Priya groups the staff roster by birth month, finds months with two or more people, and schedules a celebration for each qualifying month.
Understanding the Concepts
What does it mean to find employees sharing a birthday month?
Sharing a birthday month means two or more employees have a birth date in the same calendar month, regardless of the day or year. The java.time.Month enum (JANUARY through DECEMBER) provides the grouping key. Grouping by Month and then filtering for groups with more than one member identifies the months where multiple employees share birthdays.
This is a classic group-then-filter pattern that appears frequently in stream interview questions.
How do you implement it with streams?
The solution has three parts: guard nulls, group by month, and filter for shared months.
package com.bank.hr;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.Month;
import java.util.*;
import java.util.stream.Collectors;
public class SharedBirthdayMonth {
record Employee(String id, String name, LocalDate birthDate,
String department, BigDecimal salary) {}
public static void main(String[] args) {
List<Employee> staff = List.of(
new Employee("SAV2001", "Rajesh", LocalDate.of(1989, 1, 22), "TRADING", new BigDecimal("1800000")),
new Employee("SAV2002", "Priya", LocalDate.of(1995, 1, 5), "RISK", new BigDecimal("1200000")),
new Employee("SAV2003", "Amit", LocalDate.of(1990, 3, 15), "COMPLIANCE", new BigDecimal("900000")),
new Employee("SAV2004", "Kavita", LocalDate.of(1985, 7, 10), "OPERATIONS", new BigDecimal("1100000")),
new Employee("SAV2005", "Meera", LocalDate.of(1986, 7, 25), "TRADING", new BigDecimal("1500000")),
new Employee("SAV2006", "Ananya", LocalDate.of(1992, 5, 8), "RISK", new BigDecimal("1050000"))
);
// Step 1: Group by birth month, guarding nulls
Map<Month, List<Employee>> byMonth = staff.stream()
.filter(e -> e.birthDate() != null)
.collect(Collectors.groupingBy(e -> e.birthDate().getMonth()));
// Step 2: Keep only months with 2+ employees
Map<Month, List<Employee>> shared = byMonth.entrySet().stream()
.filter(entry -> entry.getValue().size() > 1)
.collect(Collectors.toMap(
Map.Entry::getKey, Map.Entry::getValue,
(a, b) -> a, TreeMap::new)); // TreeMap for chronological order
// Step 3: Display
shared.forEach((month, employees) -> {
System.out.println(month + ":");
employees.forEach(e -> System.out.println(
" " + e.name() + " (" + e.birthDate() + ")"));
});
}
}
JANUARY:
Rajesh (1989-01-22)
Priya (1995-01-05)
JULY:
Kavita (1985-07-10)
Meera (1986-07-25)
How do you get a flat list of employees who share a month?
If you need a single List rather than the grouped map, use flatMap on the filtered groups.
List<Employee> sharingEmployees = shared.values().stream()
.flatMap(Collection::stream)
.toList();
System.out.println("Employees sharing a birthday month: " + sharingEmployees.size());
Employees sharing a birthday month: 4
How is grouping by Month different from grouping by MonthDay?
| Aspect | Month | MonthDay |
|---|---|---|
| Grouping key | Calendar month only (JANUARY) | Month + day (--01-22) |
| Matches | Same month, any day | Exact same month and day |
| Use case | Birthday month celebration | Exact birthday twins |
| Key type | java.time.Month enum | java.time.MonthDay |
// Group by exact month-day for birthday twins
Map<java.time.MonthDay, List<Employee>> byMonthDay = staff.stream()
.filter(e -> e.birthDate() != null)
.collect(Collectors.groupingBy(
e -> java.time.MonthDay.from(e.birthDate())));
What are common mistakes?
Mistake 1: Including months with only one employee.
// WRONG: no filter, shows all months including singletons
byMonth.forEach((month, emps) -> {
System.out.println(month + ": " + emps.size());
});
// CORRECT: filter for size > 1 to find actual sharing
Map<Month, List<Employee>> shared = byMonth.entrySet().stream()
.filter(e -> e.getValue().size() > 1)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
Mistake 2: Not guarding null birth dates.
// WRONG: NullPointerException if birthDate is null
staff.stream().collect(Collectors.groupingBy(
e -> e.birthDate().getMonth())); // throws NPE
// CORRECT: filter nulls first
staff.stream()
.filter(e -> e.birthDate() != null)
.collect(Collectors.groupingBy(e -> e.birthDate().getMonth()));
What about ordering the output chronologically?
The default groupingBy produces a HashMap, which has no guaranteed order. To get months in calendar order, use a TreeMap supplier.
Map<Month, List<Employee>> byMonthOrdered = staff.stream()
.filter(e -> e.birthDate() != null)
.collect(Collectors.groupingBy(
e -> e.birthDate().getMonth(),
TreeMap::new,
Collectors.toList()));
// Keys iterate in natural Month enum order: JANUARY, FEBRUARY, ...
What about edge cases?
Edge case 1: All employees born in the same month.
// All in JANUARY: shared map has one entry with all employees
Edge case 2: No two employees share a month.
// shared map is empty after filtering
Edge case 3: Employee list is empty.
// byMonth is an empty map, shared is also empty, no output
What are common interview follow-up questions?
- "How do you find employees sharing the exact same birthday (month + day)?" Group by
MonthDay.from(birthDate)instead ofMonth. - "How do you count how many months have shared birthdays?"
shared.size()gives the count directly. - "How do you find the month with the most shared birthdays?"
shared.entrySet().stream().max(comparingInt(e -> e.getValue().size())). - "How do you handle employees with no birth date on file?" Filter them out before grouping, or collect them separately for a data-quality report.
Visual Overview
Input: [Rajesh(Jan), Priya(Jan), Amit(Mar), Kavita(Jul), Meera(Jul), Ananya(May)]
│
│ filter(birthDate != null)
│ groupingBy(birthDate.getMonth())
▼
┌──────────────────────────────────────────┐
│ JANUARY → [Rajesh, Priya] (2) ✓ │
│ MARCH → [Amit] (1) ✗ │
│ MAY → [Ananya] (1) ✗ │
│ JULY → [Kavita, Meera] (2) ✓ │
└──────────────────────────────────────────┘
│
│ filter(size > 1)
▼
┌──────────────────────────────────────────┐
│ JANUARY → [Rajesh, Priya] │
│ JULY → [Kavita, Meera] │
└──────────────────────────────────────────┘
Key Takeaway
Finding employees who share a birthday month is a straightforward group-then-filter problem. You group by the Month enum extracted from each employee's birth date, producing a map of month to employee list, and then filter that map to keep only the entries where the list has more than one member. The result tells you which months have celebrations for multiple employees.
At the National Bank of India, Priya uses this pattern to plan birthday celebrations. She guards against null birth dates before grouping, uses a TreeMap supplier so the output comes in chronological order from January to December, and flatMaps the result when she needs a single list of all sharing employees.
The pattern generalizes to any attribute: replace month with city, department, or salary band and the same group-then-filter shape applies. For exact same birthday (month and day), swap Month for MonthDay. For same full date including year, group by LocalDate itself.
Remember one sentence: groupingBy(birthMonth) then filter(size > 1) finds months where multiple employees share birthdays.
---