🔴 The Error You're Seeing
Confirm this matches your console output. If it does, you're in the right place.
java.lang.IllegalStateException: Duplicate key k (attempted merging values 1 and 2)
at java.base/java.util.stream.Collectors.duplicateKeyException(Collectors.java:135)
at java.base/java.util.stream.Collectors.lambda$uniqKeysMapAccumulator$0(Collectors.java:182)
at java.base/java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)⚡ Quick Fix Works 80% of the time
Supply a merge function as the third argument to toMap.
Map<Integer, String> m = rows.stream()
.collect(Collectors.toMap(Row::id, Row::name, (a, b) -> b));🧠 Why this Happens
Tap to expand the deep technical explanation
toMap uses uniqKeysMapAccumulator: putIfAbsent(key, value), then a null-check on any previous mapping. A second arrival with an equal key finds non-null previous and calls duplicateKeyException, reporting old and new values. This is deliberate design: keeping first-or-last silently would hide data-quality bugs, so the JDK forces you to state a merge policy explicitly.
The HITEC City Parking Spot Analogy:
Two employees named Raj clock into a locker system with one slot per key: it halts and shows both time cards instead of guessing whose shift counts.
🔁 How to Reproduce Confirm this is your error
Stream.of(1, 2).collect( Collectors.toMap(x -> "k", x -> x)); // duplicate key "k"
🛠️ Solutions (5 Ways to Fix)
Add a merge function: (a, b) -> a or b
👉 Use this if duplicates are expected data and a deterministic winner exists.
The third argument decides collisions: keep-first, keep-latest, min/max by comparator - any BinaryOperator works.
Collectors.toMap(User::id, User::name,
(existing, replacement) -> existing)Merge semantically: Integer::sum
👉 Use this if duplicates should combine rather than compete, e.g. quantity per product.
Merge functions are full reducers: summing counts, concatenating strings, or max-ing timestamps expresses intent instead of hiding it.
Collectors.toMap(Sale::sku, Sale::qty, Integer::sum)groupingBy when multiples belong together
👉 Use this if repeated keys mean you actually wanted a Map<K, List<V>>.
groupingBy collects every value per key downstream, eliminating the collision conceptually instead of arbitrating it.
import static java.util.stream.Collectors.groupingBy;
Map<Integer, List<Order>> byCust = orders.stream()
.collect(groupingBy(Order::customerId));Preserve order with a LinkedHashMap supplier
👉 Use this if the merged map must keep encounter order via the four-arg toMap overload.
The fourth argument supplies the map implementation; LinkedHashMap keeps insertion order while the merge function handles collisions.
Collectors.toMap(k, v, (a, b) -> b, LinkedHashMap::new)De-duplicate upstream before collecting
👉 Use this if duplicates indicate dirty source data worth surfacing early.
Filter to one record per key so toMap receives unique keys; the exception doubles as an integrity alarm when uniqueness was assumed.
Set<Integer> seen = new HashSet<>();
list.stream()
.filter(u -> seen.add(u.id()))
.collect(Collectors.toMap(User::id, u -> u));📋 Version Notes
Message was just "Duplicate key k" - key only, no values.
Message extended with "(attempted merging values X and Y)" naming both colliding values; behavior otherwise unchanged through Java 21+.
🛡️ How to Prevent This Next Time
Never write a two-argument toMap against data you do not control. Decide first: merge, group, or guarantee uniqueness upstream.