SSerguey Asael Shinder
Java coding notes: the JVM, and writing software that lasts

Serguey Asael Shinder: Map.of throws on a duplicate key while put keeps the last value

· by Serguey Asael Shinder / Serguey Shinder

These two blocks look like the same map written two ways:

Map<String, Integer> limits = new HashMap<>();
limits.put("eu", 100);
limits.put("us", 200);
limits.put("eu", 150);          // replaces 100, no error
Map<String, Integer> limits2 = Map.of(
        "eu", 100,
        "us", 200,
        "eu", 150);             // IllegalArgumentException
Serguey Asael Shinder: Map.of throws on a duplicate key while put keeps the last value
Map.of throws on a duplicate key while put keeps the last value — Serguey Asael Shinder

They are not. The javadoc of Map.put says that if the map already contains a mapping for the key, "the old value is replaced by the specified value", and the method returns the previous value. The Map.of overloads list a different contract under Throws: "IllegalArgumentException - if there are any duplicate keys" and "NullPointerException - if any key or value is null". The section on unmodifiable maps repeats it for Map.of, Map.ofEntries and Map.copyOf: such maps "reject duplicate keys at creation time. Duplicate keys passed to a static factory method result in IllegalArgumentException." (https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Map.html)

The trap is the refactor. A configuration map, a lookup table or a test fixture built with a chain of put calls tolerates a copy-pasted key silently: the later value wins and the earlier line is dead code. Convert it to Map.of because it reads better and is immutable, and the same typo now fails, but at the moment the class or method that builds the map first runs.

That failure is the better behaviour; a duplicate key in a literal is almost always a mistake. The point is to expect it. When you convert, search the old put chain for repeated keys first, and decide which value was meant, because the old code was already using the last one. When the duplicates are intentional, for example defaults followed by overrides, keep a mutable map or merge explicitly instead of hiding the order in a literal.

The same family has a stream version with yet another exception: Collectors.toMap without a merge function throws IllegalStateException on a key collision, covered here on 22 September. Three APIs, three behaviours for one situation: replace, IllegalArgumentException, IllegalStateException.