Java interview question
Using Java streams, find the duplicate values in a list and count their occurrences.
Short answer
Group with Collectors.groupingBy(Function.identity(), Collectors.counting()) and keep entries with a count above one. For just the duplicate values, add to a HashSet and collect elements whose add returns false. Explain complexity, ordering (LinkedHashMap) and when a plain loop is clearer.
Answer
This is a common live-coding warm-up. The idiomatic stream answer groups elements by themselves and counts them, then keeps the groups with a count greater than one. It runs in linear time, O(n), with O(n) extra space.
Count every duplicate
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
public class Duplicates {
public static void main(String[] args) {
List<String> emails = List.of(
"a@x.com", "b@x.com", "a@x.com", "c@x.com", "b@x.com", "a@x.com");
Map<String, Long> duplicates = emails.stream()
.collect(Collectors.groupingBy(
Function.identity(),
LinkedHashMap::new, // keep first-seen order
Collectors.counting()))
.entrySet().stream()
.filter(e -> e.getValue() > 1)
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(x, y) -> x,
LinkedHashMap::new));
System.out.println(duplicates); // {a@x.com=3, b@x.com=2}
}
}
The two-argument groupingBy(Function.identity(), Collectors.counting()) returns a HashMap with no guaranteed order. Passing LinkedHashMap::new makes the output deterministic, which matters if you assert on it. Use TreeMap::new for sorted output.
Just the duplicate values
A Set.add call returns false if the element was already present:
Set<String> seen = new HashSet<>();
Set<String> dups = emails.stream()
.filter(e -> !seen.add(e))
.collect(Collectors.toCollection(LinkedHashSet::new));
// [a@x.com, b@x.com]
This is concise, but it relies on a side effect inside a lambda, which is only safe for sequential streams. With .parallel() the shared HashSet is not thread-safe. Some interviewers will probe this; the groupingBy version has no such issue.
A plain loop is also a good answer
Map<String, Integer> counts = new LinkedHashMap<>();
for (String e : emails) {
counts.merge(e, 1, Integer::sum);
}
counts.values().removeIf(c -> c < 2);
Being able to say “the loop is just as fast and arguably easier to read” shows judgement rather than stream fixation.
Custom objects
For a list of objects, either group by a key (groupingBy(User::email, counting())) or make sure the class implements equals and hashCode correctly. A record does this automatically. Without it, every element is unique by identity and you find no duplicates.
Normalisation
Real-world duplicate checks usually need normalisation first, for example .map(s -> s.trim().toLowerCase(Locale.ROOT)) for emails. Mention it: it shows you think about the data, not just the algorithm.
Where this shows up in testing
- Asserting that an API returns no duplicate IDs:
assertEquals(ids.size(), new HashSet<>(ids).size()), with thegroupingByresult in the failure message so you can see which IDs repeat. - Checking test data generators produce unique emails.
What interviewers look for
- A working
groupingBypluscountingsolution and a filter on count. - Awareness of ordering and the
LinkedHashMapoverload. - Complexity stated correctly.
- Understanding of side effects in parallel streams and the role of
equals/hashCode.
A strong answer covers
- Uses groupingBy with counting
- Filters entries with count greater than one
- Alternative using a Set where add() returns false
- Discusses complexity and ordering
- Relies on correct equals/hashCode for custom objects