Back to Blog
Java

How Java HashMap Handles Duplicate Keys

java hashmap duplicate keys: Learn how Java HashMap handles duplicate keys: replacement behavior, putIfAbsent, multi-value maps, custom key equals/hashCode, and concur...

HashMapJava CollectionsMap APIputIfAbsentequals and hashCodeConcurrency
Illustration of a Java HashMap replacing an existing value when a duplicate key is inserted, showing the old value being returned.

Understanding how java hashmap duplicate keys behave is a common source of confusion for Java developers. A HashMap does not allow duplicate keys: when you call put() with a key that already exists in the map, the new value replaces the old value, and put() returns the previous value. This replacement behavior is the default, and knowing exactly when it happens prevents subtle bugs in value handling, conditional updates, and multi-value accumulation.

Map<String, Integer> scores = new HashMap<>(); scores.put("alice", 90); Integer previous = scores.put("alice", 95); System.out.println(previous); // 90 System.out.println(scores.get("alice")); // 95 System.out.println(scores.size()); // 1

The map still contains only one entry for "alice". The second put() call did not create a second entry; it overwrote the existing value and returned the previous one.

What put() Returns on Duplicate Keys

The put() method returns the value that was previously associated with the key, or null if there was no previous mapping. This return value is useful when you need to know whether an insertion actually replaced an existing entry.

Map<String, String> config = new HashMap<>(); String first = config.put("timeout", "30"); String second = config.put("timeout", "60"); System.out.println(first); // null System.out.println(second); // "30"

The first call returns null because "timeout" was not present. The second call returns "30" because that was the previous value. Note that put() returning null does not necessarily mean the key was absent — it can also mean the previous value was null. If you need to distinguish those cases, use containsKey() before calling put().

Checking Whether a Key Already Exists

If you want to insert a value only when the key is absent, use putIfAbsent() instead of checking containsKey() and then calling put(). The putIfAbsent() method performs the check and the insert in a single call, which avoids the race condition that a separate check-then-act sequence would introduce in concurrent code.

Map<String, Integer> counters = new HashMap<>(); counters.put("requests", 1); counters.putIfAbsent("requests", 100); counters.putIfAbsent("errors", 1); System.out.println(counters.get("requests")); // 1 System.out.println(counters.get("errors")); // 1

putIfAbsent() returns the existing value if the key was present, or null if the key was newly inserted. This is a cleaner alternative to the explicit check-then-act pattern and works correctly under concurrency when used with ConcurrentHashMap.

When You Actually Need Multiple Values per Key

Sometimes the requirement is not to replace a value but to accumulate multiple values under the same key. A HashMap cannot do this directly. The standard approach is to use a Map<K, List<V>> and manage the list yourself.

Map<String, List<String>> groups = new HashMap<>(); groups.computeIfAbsent("admins", k -> new ArrayList<>()).add("alice"); groups.computeIfAbsent("admins", k -> new ArrayList<>()).add("bob"); System.out.println(groups.get("admins")); // [alice, bob]

computeIfAbsent() creates the list only when the key is missing and returns the existing list otherwise. This avoids the verbose pattern of checking containsKey(), creating a list, and putting it back. If you need this pattern frequently, consider a library such as Guava's Multimap, which provides ArrayListMultimap and similar collections that handle the list management internally.

Custom Keys and the equals()/hashCode() Contract

Duplicate keys can appear to exist when custom objects are used as keys and the equals() and hashCode() methods are not implemented correctly. Two distinct object instances that are logically equal will be treated as separate keys if they produce different hash codes or if equals() returns false.

class User { String id; User(String id) { this.id = id; } // No equals() or hashCode() override } Map<User, String> roles = new HashMap<>(); roles.put(new User("u1"), "admin"); roles.put(new User("u1"), "editor"); System.out.println(roles.size()); // 2

Without overriding equals() and hashCode(), each new User("u1") is a distinct object, so the map contains two entries even though the id fields match. Overriding both methods fixes this:

class User { String id; User(String id) { this.id = id; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof User)) return false; return id.equals(((User) o).id); } @Override public int hashCode() { return id.hashCode(); } }

Now two User instances with the same id are treated as the same key, and the second put() replaces the first value instead of adding a new entry.

Performance Implications of Key Collisions

When keys collide — meaning they produce the same hash code — HashMap stores them in the same bucket. In Java 8 and later, a bucket that grows beyond a threshold is converted from a linked list to a red-black tree, which keeps lookup time at O(log n) rather than O(n) for that bucket.

This matters for duplicate keys in a different way: if many keys are logically distinct but share the same hash code, lookups degrade. A well-implemented hashCode() that distributes values evenly across buckets keeps get() and put() at near-constant time.

For keys that are String or primitive wrappers, the default hash implementations are already well distributed. For custom keys, the hashCode() implementation directly affects how often collisions occur, which in turn affects how quickly the map responds under heavy load.

Concurrency and Duplicate Key Behavior

The standard HashMap is not thread-safe. Concurrent modification can corrupt the internal structure, and the duplicate-key behavior described above is not guaranteed under concurrent access. Use ConcurrentHashMap when multiple threads read and write the same map.

ConcurrentHashMap follows the same duplicate-key rule: a put() with an existing key replaces the value. It also provides atomic methods such as putIfAbsent() and compute() that are safe to call from multiple threads.

ConcurrentHashMap<String, Integer> counters = new ConcurrentHashMap<>(); counters.put("hits", 1); counters.compute("hits", (key, value) -> value == null ? 1 : value + 1);

The compute() method applies the remapping function atomically, which avoids the lost-update problem that would occur with a check-then-act sequence on a plain HashMap.

Choosing Between Replace, Merge, and Compute

When you need to update a value conditionally, Java provides several methods that handle the duplicate-key case differently:

MethodBehavior when key existsBehavior when key is absent
put()Replaces value, returns old valueInserts, returns null
putIfAbsent()Does nothing, returns existing valueInserts, returns null
merge()Applies remapping function to old and new valuesInserts new value
compute()Applies remapping function to existing valueApplies function to null

merge() is particularly useful for accumulating values:

Map<String, Integer> totals = new HashMap<>(); totals.merge("sales", 10, Integer::sum); totals.merge("sales", 5, Integer::sum); System.out.println(totals.get("sales")); // 15

The remapping function receives the old value and the new value, and its result becomes the stored value. This is a concise way to handle the duplicate-key scenario when you want to combine values rather than replace them. Use merge() when the new value should be combined with the old one, putIfAbsent() when the first value should win, and put() when the latest value should win unconditionally.

java hashmap duplicate keys: Practical Usage and Code Exampl | RYUSLOG DEV