Why Collectors.toMap throws "Duplicate key", and how to fix it
toMap refuses to guess which value you want when a key repeats. Give it a merge function and the crash goes away.
On this page ⌄
A collector decides the shape of your result: a map, two groups, or one string. toMap builds a lookup table that links each key to one value. That "one" is where the crash comes from.
The setup
Three students and their test scores:
record Score(String name, int score) {}
List<Score> scores = List.of(
new Score("Asha", 82),
new Score("Ravi", 45),
new Score("Meena", 67));
Map<String, Integer> byName = scores.stream()
.collect(Collectors.toMap(Score::name, Score::score));
// {Asha=82, Ravi=45, Meena=67}The crash
Asha takes the test again and scores 90. Now her name appears twice:
List<Score> scores = List.of(
new Score("Asha", 82), new Score("Ravi", 45),
new Score("Meena", 67), new Score("Asha", 90));
scores.stream().collect(Collectors.toMap(Score::name, Score::score));
// IllegalStateException: Duplicate key Asha (attempted merging values 82 and 90)A map keeps one value per key, and toMap won't guess which score you want.
The fix: a merge function
Pass a third argument. It decides what happens when a key repeats:
Map<String, Integer> best = scores.stream()
.collect(Collectors.toMap(Score::name, Score::score, Math::max));
// {Ravi=45, Asha=90, Meena=67}Math::max keeps the higher score. Other common choices:
(a, b) -> akeeps the first value(a, b) -> bkeeps the last valueInteger::sumadds them up
Bonus: partitioningBy and joining
partitioningBy always gives you exactly two groups, true and false:
Map<Boolean, List<String>> passed = scores.stream()
.collect(Collectors.partitioningBy(
s -> s.score() >= 50,
Collectors.mapping(Score::name, Collectors.toList())));joining glues strings into one, with an optional prefix and suffix:
Stream.of("Asha", "Ravi", "Meena").collect(Collectors.joining(", ", "[", "]"));
// [Asha, Ravi, Meena]Get the next one in your inbox
New posts, videos and free PDFs. One email a week at most.