Definition
- groupingBy(getName, counting()) produces
Mapwhere each name maps to how many times it appeared. - Filter count > 1 on the map's entrySet stream keeps only names that appear more than once.
- Set trick:
!seen.add(name)returns true for the second and later occurrences, detecting duplicates in one pass, but requires a mutable shared Set (not parallel-safe). - groupingBy(getName, toList()) collects the actual Employee objects per name, so you can return the full duplicate records, not just names.
- Case normalization: use
groupingBy(e -> e.getName().toUpperCase(), counting())to treat "Amit" and "amit" as the same name.
The National Bank of India payroll auditor suspects some employees were entered twice after a data migration. She sorts all name cards into piles: one pile per name. Then she looks for any pile with more than one card. Those piles are the duplicates. Grouping by name and filtering for counts above one is exactly this process.
Understanding the Concepts
What does this question ask?
The interviewer wants you to find elements that appear more than once based on a specific field. This tests groupingBy with counting, map entry filtering, and awareness of the single-pass Set trick.
How do you find duplicate names using groupingBy?
package com.bank.hr;
import java.util.*;
import java.util.stream.Collectors;
public class FindDuplicateNames {
public static void main(String[] args) {
List<Employee> employees = List.of(
new Employee("E001", "Rajesh", "TRADING", 45),
new Employee("E002", "Priya", "RISK", 28),
new Employee("E003", "Amit", "OPERATIONS", 35),
new Employee("E004", "Rajesh", "COMPLIANCE", 52),
new Employee("E005", "Priya", "TRADING", 31),
new Employee("E006", "Kavita", "RISK", 40)
);
// Step 1: count occurrences per name
Map<String, Long> nameCounts = employees.stream()
.collect(Collectors.groupingBy(
Employee::getName, Collectors.counting()));
// Step 2: filter names with count > 1
List<String> duplicateNames = nameCounts.entrySet().stream()
.filter(e -> e.getValue() > 1)
.map(Map.Entry::getKey)
.toList();
System.out.println("Duplicate names: " + duplicateNames);
}
}
Duplicate names: [Rajesh, Priya]
How do you get the actual duplicate Employee objects?
Use toList() instead of counting() as the downstream, then filter groups with size > 1.
// Get full Employee objects for duplicates
Map<String, List<Employee>> grouped = employees.stream()
.collect(Collectors.groupingBy(Employee::getName));
List<Employee> duplicateEmployees = grouped.entrySet().stream()
.filter(e -> e.getValue().size() > 1)
.flatMap(e -> e.getValue().stream())
.toList();
duplicateEmployees.forEach(e ->
System.out.println(e.getId() + ": " + e.getName()
+ " - " + e.getDepartment()));
E001: Rajesh - TRADING
E004: Rajesh - COMPLIANCE
E002: Priya - RISK
E005: Priya - TRADING
How does the single-pass Set trick work?
The Set.add() method returns false when the element was already present. Negating it with ! gives true for duplicates.
Set<String> seen = new HashSet<>();
List<Employee> duplicatesOnePas = employees.stream()
.filter(e -> !seen.add(e.getName())) // false on first, true on second+
.toList();
duplicatesOnePas.forEach(e ->
System.out.println(e.getName() + " (duplicate)"));
Rajesh (duplicate)
Priya (duplicate)
This returns only the second and later occurrences. The first occurrence passes through seen.add() returning true, so !true is false and the element is filtered out.
How is the two-pass approach different from the Set trick?
| Feature | groupingBy + filter (two-pass) | Set trick (single-pass) |
|---|---|---|
| Passes over data | 2 (group, then filter map) | 1 |
| Provides count | Yes (how many times each duplicated) | No |
| Parallel-safe | Yes (stateless collectors) | No (shared mutable Set) |
| Returns | All occurrences including first | Only second and later occurrences |
| Production use | Preferred | Quick interview trick |
// Two-pass: gets ALL Rajesh entries (both first and duplicate)
Map<String, List<Employee>> allDuplicates = employees.stream()
.collect(Collectors.groupingBy(Employee::getName))
.entrySet().stream()
.filter(e -> e.getValue().size() > 1)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
// Set trick: gets only the SECOND Rajesh, not the first
Set<String> seen2 = new HashSet<>();
List<Employee> onlyLater = employees.stream()
.filter(e -> !seen2.add(e.getName()))
.toList();
What are common mistakes?
Mistake 1: using the Set trick with parallel streams.
// WRONG: HashSet is not thread-safe
Set<String> seen = new HashSet<>();
List<Employee> wrong = employees.parallelStream()
.filter(e -> !seen.add(e.getName())) // data race!
.toList();
// CORRECT for parallel: use groupingBy (stateless)
List<String> correct = employees.parallelStream()
.collect(Collectors.groupingBy(Employee::getName, Collectors.counting()))
.entrySet().stream()
.filter(e -> e.getValue() > 1)
.map(Map.Entry::getKey)
.toList();
Mistake 2: confusing "find duplicates" with "remove duplicates".
// REMOVING duplicates (keep unique): use distinct or distinctByKey
List<Employee> unique = employees.stream()
.distinct() // removes duplicates, keeps one of each
.toList();
// FINDING duplicates (identify repeated): use groupBy + count > 1
List<String> duplicated = employees.stream()
.collect(Collectors.groupingBy(Employee::getName, Collectors.counting()))
.entrySet().stream()
.filter(e -> e.getValue() > 1)
.map(Map.Entry::getKey)
.toList();
How do you handle case-insensitive duplicate detection?
// "Rajesh" and "rajesh" treated as same name
Map<String, Long> caseInsensitive = employees.stream()
.collect(Collectors.groupingBy(
e -> e.getName().toUpperCase(),
Collectors.counting()));
List<String> dupes = caseInsensitive.entrySet().stream()
.filter(e -> e.getValue() > 1)
.map(Map.Entry::getKey)
.toList();
What are common interview follow-up questions?
- "How do you find elements that appear exactly N times?" Change the filter to
count == N. - "How do you find duplicates across multiple fields?" Use a composite key:
groupingBy(e -> e.getName() + "|" + e.getDepartment()). - "How do you return the duplicate count along with the names?" Return the filtered map entries directly instead of mapping to keys only.
- "How do you find the most frequently duplicated name?" After filtering count > 1, use
max(comparingByValue())on the entries.
Visual Overview
employees:
[Rajesh-TRADING, Priya-RISK, Amit-OPS, Rajesh-COMP, Priya-TRADING, Kavita-RISK]
│
│ groupingBy(getName, counting())
▼
┌────────────────────────────┐
│ Name │ Count │
│ Rajesh │ 2 ← duplicate │
│ Priya │ 2 ← duplicate │
│ Amit │ 1 │
│ Kavita │ 1 │
└────────────────────────────┘
│
│ filter(count > 1)
▼
[Rajesh, Priya] ← duplicate names
Key Takeaway
Finding duplicates by a field uses two steps: group by the field with counting, then filter the map for counts greater than one. The expression groupingBy(Employee::getName, counting()) produces a frequency map, and streaming its entries with filter(e -> e.getValue() > 1) identifies the duplicate names.
The single-pass Set trick (!seen.add(name)) is elegant for sequential streams: add returns false when the element was already in the set, so negating it gives true for duplicates. However, it is stateful and not safe for parallel streams because the shared HashSet has no thread safety.
For production code at the National Bank of India, the two-pass groupingBy approach is preferred because it is stateless, works correctly in parallel, and provides the exact count of repetitions. The count itself is useful: the auditor can prioritize names that appear five times over those that appear twice.
Remember one sentence: group by the key with counting, then filter for count greater than one to find duplicates, and prefer this stateless approach over the Set trick for parallel safety.
---