HashMap Doubts Explained in Java: Keys, Retrieval, Equality, Mutability, Nulls, and Collisions

  • Last Updated: July 30, 2026
  • By: javahandson
  • Series
img

HashMap Doubts Explained in Java: Keys, Retrieval, Equality, Mutability, Nulls, and Collisions

HashMap Doubts Explained in Java with clear answers on keys, hashing, equals(), hashCode(), nulls, collisions, retrieval, resizing, and custom keys.

Most HashMap explanations begin with buckets and hashing. Those ideas matter, but the doubts that actually stop developers are usually more concrete: Does the map store my key object or a copy? Does an equal lookup key replace the stored key? Can I change a String key? Why is only one null key allowed? What exactly breaks when a custom key is mutable?

This guide answers those questions in the same beginner-friendly, numbered, code-first format as the attached HashMap article. It keeps the original mental model of buckets, nodes, hashing, equals(), and hashCode(), then expands it with the full doubt list developed in this conversation.

Before we begin, keep these headline facts nearby. Almost every answer in the article follows from them.

  • HashMap stores references to the original key and value objects; it does not make defensive copies.
  • A key is unique according to hashCode() plus equals(), not according to object identity alone.
  • get() only searches; it does not insert, replace, or override a key.
  • Putting an equal key replaces the value associated with that logical key.
  • Fields used by equals() and hashCode() must remain stable while the object is used as a key.
  • HashMap permits one null key because keys are unique, while values may repeat, including null.

1. One Mental Model That Answers Most Doubts

Imagine a HashMap as an array of buckets. Each stored mapping is wrapped in a node containing a cached hash, the key reference, the value reference, and a link to another node when a collision occurs.

static class Node<K, V> {
    final int hash;
    final K key;
    V value;
    Node<K, V> next;
}

The crucial field for your doubts is key. It contains the reference to the key object supplied to put(). HashMap does not clone that object. It remembers the object reference and the hash calculated at insertion time.

Every operation then repeats the same rhythm: calculate a hash, choose a bucket, and use equals() to identify the exact logical key inside that bucket.

Interview Insight
A concise mental model is: hashCode() takes HashMap to the likely bucket; equals() identifies the exact key inside that bucket; the node stores the original key reference and its associated value.

2. Does HashMap Store the Original Key Object or a Copy?

HashMap stores the original key object reference. It does not create a copy, clone, snapshot, or serialized form of the key.

EmployeeKey key = new EmployeeKey(101, "Engineering");
 
Map<EmployeeKey, String> employees = new HashMap<>();
employees.put(key, "Alice");
 
EmployeeKey keyFromMap = employees.keySet().iterator().next();
System.out.println(key == keyFromMap); // true

Both the local variable key and the node inside the map refer to the same EmployeeKey object. That behavior is efficient, but it is also the reason mutable keys are dangerous.

2.1 Reassigning the Variable Is Not Mutating the Stored Key

EmployeeKey key = new EmployeeKey(101, "Engineering");
employees.put(key, "Alice");
 
key = new EmployeeKey(202, "Finance");

The assignment changes only the local variable. The map still refers to the original EmployeeKey(101, “Engineering”) object. Reassignment changes where the variable points; mutation changes the state of the object itself.

EmployeeKey key = new EmployeeKey(101, "Engineering");
employees.put(key, "Alice");

Local variable key ───────────┐
                              ▼
                EmployeeKey(101, "Engineering")
                              ▲
HashMap Node.key ─────────────┘

Both references initially point to the same object. Then you execute:

key = new EmployeeKey(202, "Finance");

This changes only the local variable’s reference:

Local variable key ─────────────► EmployeeKey(202, "Finance")
HashMap Node.key ────────────────► EmployeeKey(101, "Engineering")

2.2 Mutating the Object Changes the Object Seen by the Map

Because the map stores the same reference, modifying a mutable key modifies the key object held by the node. HashMap does not move the node automatically when the key changes. The node remains in the bucket selected using the old hash.

Common Doubt
HashMap does not “freeze” an object when it becomes a key. Immutability must come from the key class itself, through final fields, no mutators, and immutable component values.

3. What Exactly Happens During put(key, value)?

The put() method calculates the spread hash, finds the bucket, and then decides whether it is adding a new logical key or updating an existing logical key.

Map<EmployeeKey, String> employees = new HashMap<>();
EmployeeKey firstKey = new EmployeeKey(101, "Engineering");
employees.put(firstKey, "Alice");

For the first insertion, the bucket is empty, so HashMap creates a new node containing firstKey and “Alice”.

3.1 Putting a Different Object That Is Logically Equal

EmployeeKey secondKey = new EmployeeKey(101, "Engineering");
employees.put(secondKey, "Bob");

firstKey and secondKey are different objects, but a correctly implemented EmployeeKey considers them equal and gives them the same hash code. HashMap finds the existing node and replaces the node value from “Alice” to “Bob”. The size remains one.

System.out.println(firstKey == secondKey);          // false
System.out.println(firstKey.equals(secondKey));     // true
System.out.println(employees.size());               // 1
System.out.println(employees.get(firstKey));        // Bob
System.out.println(employees.get(secondKey));       // Bob

In the current OpenJDK HashMap implementation, the original stored key reference normally remains firstKey; the associated value is replaced. The Map contract mainly promises replacement of the mapping, so interview answers should emphasize that the value is updated and no second logical key is created.

3.2 Same Hash Code but equals() Is False

Now suppose two different keys produce the same hash code but are not equal. That is a collision, not an update. HashMap keeps both nodes in the same bucket and later uses equals() to tell them apart.

final class CollisionKey {
    private final int id;
 
    CollisionKey(int id) { this.id = id; }
 
    @Override public int hashCode() { return 1; }
 
    @Override public boolean equals(Object object) {
        return object instanceof CollisionKey other && id == other.id;
    }
}
Interview Insight
Same hash code does not mean same key. It means “check the same bucket.” equals() makes the final decision about logical key equality.

4. What Exactly Happens During get(key)?

Retrieval follows nearly the same path as insertion, but it does not modify the map.

EmployeeKey storedKey = new EmployeeKey(101, "OpenAI");
employees.put(storedKey, "Alice");
 
EmployeeKey lookupKey = new EmployeeKey(101, "OpenAI");
String name = employees.get(lookupKey);

HashMap performs these steps:

  • Calculate the spread hash of lookupKey.
  • Calculate the bucket index using the table length and the hash.
  • Compare the cached hash of candidate nodes.
  • For matching hashes, compare key identity or call equals().
  • Return the value from the matching node, or null if no key matches.
storedHash == lookupHash
    && (storedKey == lookupKey || lookupKey.equals(storedKey))

4.1 Does lookupKey Override storedKey?

No. get(lookupKey) is only a search. lookupKey is a temporary lookup object. It is not inserted into the map, and it does not replace the key stored inside the node.

System.out.println(storedKey == lookupKey);       // false
System.out.println(storedKey.equals(lookupKey));  // true
System.out.println(employees.get(lookupKey));     // Alice
System.out.println(employees.size());             // 1
The Exact Answer to Your Doubt
The equal lookup object helps HashMap find the node. It does not become the node’s key. Replacement happens only through an update operation such as put(), replace(), compute(), or merge().

5. Understanding ==, equals(), and hashCode() Together

These three checks answer different questions.

  • == asks whether two references point to the exact same object.
  • equals() asks whether two objects represent the same logical value.
  • hashCode() gives HashMap a number used to choose a bucket efficiently.
EmployeeKey a = new EmployeeKey(101, "OpenAI");
EmployeeKey b = new EmployeeKey(101, "OpenAI");
 
System.out.println(a == b);          // false
System.out.println(a.equals(b));     // true
System.out.println(a.hashCode() == b.hashCode()); // true

5.1 The Contract You Must Remember

  • If a.equals(b) is true, a.hashCode() and b.hashCode() must be equal.
  • Equal hash codes do not require equals() to be true. Collisions are legal.
  • The equality result and hash code must stay stable while the object is stored as a key.
  • equals() should be reflexive, symmetric, transitive, consistent, and false for null.
Classic Interview Trap
Overriding equals() without overriding hashCode() can send logically equal objects to different buckets. HashMap may then store them as separate entries or fail to retrieve a mapping with an equal lookup object.

6. Creating a Correct Custom Map Key

A good custom key behaves like a value. Two independently created instances representing the same business identity should compare equal and produce the same hash code. The state defining that identity should not change.

import java.util.Objects;
 
public record EmployeeKey(long employeeId, String organization) {
    public EmployeeKey {
        Objects.requireNonNull(organization,
                "organization must not be null");
    }
}

A record automatically supplies private final component fields, accessors, equals(), hashCode(), and toString(). Here, long is a primitive and String is immutable, so the equality state is stable.

6.2 Traditional Immutable Class

import java.util.Objects;
 
public final class EmployeeKey {
    private final long employeeId;
    private final String organization;
 
    public EmployeeKey(long employeeId, String organization) {
        this.employeeId = employeeId;
        this.organization = Objects.requireNonNull(organization);
    }
 
    @Override
    public boolean equals(Object object) {
        if (this == object) return true;
        if (!(object instanceof EmployeeKey other)) return false;
        return employeeId == other.employeeId
                && organization.equals(other.organization);
    }
 
    @Override
    public int hashCode() {
        return Objects.hash(employeeId, organization);
    }
}

The class is final, the equality fields are final, there are no setters, and both equals() and hashCode() use the same fields.

6.3 Are final Fields Enough?

final prevents a field from being reassigned after construction. It does not automatically make the referenced object immutable. This is called shallow immutability.

public record UnsafeKey(long id, List<String> departments) { }
 
List<String> departments = new ArrayList<>();
departments.add("Engineering");
UnsafeKey key = new UnsafeKey(101, departments);
map.put(key, "Alice");
 
departments.add("Finance"); // the record component state changed

The departments reference inside the record is final, but the list contents can change. Because record-generated equals() and hashCode() include the list, changing the list can change the key hash. A defensive copy solves the problem.

public record EmployeeDepartmentsKey(long id, List<String> departments) {
    public EmployeeDepartmentsKey {
        departments = List.copyOf(departments);
    }
}
Practical Rule
Use primitives, String, enums, UUID, immutable value objects, and immutable collections inside map keys. final fields are necessary for a traditional immutable key, but deep immutability of the equality state is the real goal.

7. Why String Is a Safe HashMap Key

A String object cannot be modified after creation. Methods such as concat(), replace(), and toUpperCase() return another String instead of editing the original object.

String key = "Engineering";
map.put(key, "Alice");
 
key = key.concat("-Team");
 
System.out.println(map.get("Engineering"));      // Alice
System.out.println(map.get("Engineering-Team")); // null

The variable now points to a new String, while the original “Engineering” object remains unchanged inside the map. String also implements content-based equals() and a stable hashCode(), making it an excellent key type.

Common Doubt
A String variable can be reassigned, but a String object cannot be mutated. Reassignment is not mutation.

8. The Mutable-Key Failure, Step by Step

final class MutableEmployeeKey {
    private long employeeId;
 
    MutableEmployeeKey(long employeeId) {
        this.employeeId = employeeId;
    }
 
    void setEmployeeId(long employeeId) {
        this.employeeId = employeeId;
    }
 
    @Override public boolean equals(Object object) {
        return object instanceof MutableEmployeeKey other
                && employeeId == other.employeeId;
    }
 
    @Override public int hashCode() {
        return Long.hashCode(employeeId);
    }
}
MutableEmployeeKey key = new MutableEmployeeKey(101);
Map<MutableEmployeeKey, String> map = new HashMap<>();
map.put(key, "Alice");
 
key.setEmployeeId(202);
 
System.out.println(map.get(key));    // usually null
System.out.println(map.remove(key)); // usually null
System.out.println(map.size());      // still 1

At insertion, the key hash directs the node to one bucket. After the ID changes, get() calculates a different hash and searches another bucket. The node did not move, so it becomes logically unreachable through the mutated key.

The entry still exists. Iteration may reveal it, because iteration walks the table rather than performing a fresh key lookup. This is why mutable-key bugs can look like map corruption even though the map is following its rules.

[ IMAGE PLACEHOLDER ]
File: mutable-key-lost-entry.jpg
Alt text: Before-and-after diagram showing a key inserted into bucket X using one hash and later looked up in bucket Y after its equality field is mutated

9. Why One Null Key but Multiple Null Values?

HashMap allows one null key because every key must be unique. Repeating null means repeating the same key, so the later put() replaces the earlier value.

Map<String, String> map = new HashMap<>();
map.put(null, "Alice");
map.put(null, "Bob");
 
System.out.println(map.size());    // 1
System.out.println(map.get(null)); // Bob

Values do not need to be unique. Different keys may point to the same value, including null.

map.put("A", null);
map.put("B", null);
map.put("C", null);
 
System.out.println(map.size()); // 4, including the null-key entry

Internally, HashMap gives a null key a hash of zero, which selects bucket index zero. It handles equality without calling a method on null.

9.1 Why get() Returning null Can Be Ambiguous

map.put("A", null);
 
System.out.println(map.get("A"));       // null
System.out.println(map.get("Missing")); // null

The first key exists with a null value. The second key is absent. Use containsKey() when that distinction matters.

System.out.println(map.containsKey("A"));       // true
System.out.println(map.containsKey("Missing")); // false

10. Collisions, Bucket Chains, and Treeification

A collision occurs when two different keys select the same bucket. It can happen even when their full hash codes differ, because the table index uses only enough bits for the current capacity.

HashMap first stores colliding nodes in a linked structure. In Java 8 and later, a heavily populated bucket may become a red-black tree. The commonly discussed implementation thresholds are eight nodes for treeification, six for converting a small tree back to a list, and a minimum table capacity of 64 before treeification is preferred over resizing.

  • A collision does not overwrite an entry unless the keys are equal.
  • A poor hashCode() increases bucket crowding and slows operations.
  • A constant hashCode() can still be functionally correct when equals() is correct, but performance suffers.
  • Average put(), get(), and remove() are O(1) with good distribution.
  • A long linked bucket is O(n); a tree bucket is approximately O(log n).
Interview Insight
hashCode() is a routing hint, not a unique identity. equals() remains the authority for deciding whether a candidate node represents the requested key.

11. Resizing, Capacity, and Load Factor

HashMap keeps its table length as a power of two. The logical default capacity is 16, the default load factor is 0.75, and the internal table is allocated lazily when the map first needs storage.

threshold = capacity × loadFactor
          = 16 × 0.75
          = 12

When an insertion makes size exceed the threshold, the table normally doubles. During a Java 8-style resize, each node either stays at its old bucket index or moves to oldIndex + oldCapacity. Its cached hash does not need to be recalculated from the key.

  • A lower load factor uses more memory but generally reduces collisions.
  • A higher load factor saves table space but allows denser buckets.
  • A sensible initial capacity can avoid repeated resizes when the expected entry count is known.
  • An unnecessarily huge capacity wastes memory and can make iteration slower because iteration depends on capacity plus size.
Interview Insight
Resizing is an occasional O(n) operation, while ordinary inserts are expected O(1). That is why HashMap offers amortized constant-time insertion rather than claiming every individual put() costs exactly O(1).

12. Retrieval and Update APIs That Often Cause Confusion

12.1 put()

Adds a new logical key or replaces the value for an equal existing key. It returns the previous value, or null when there was no previous mapping or the previous value itself was null.

12.2 putIfAbsent()

Adds the value only when the key is absent or currently mapped to null. This null behavior surprises developers who read “absent” as containsKey() being false only.

12.3 replace()

Updates an existing mapping but does not add a missing key. Overloads can replace unconditionally for an existing key or conditionally when both the old key and old value match.

12.4 computeIfAbsent()

Calculates a value only when the key is absent or mapped to null. It is useful for caches and grouping, but the mapping function should not structurally modify the same map.

12.5 getOrDefault()

Returns the mapped value when a mapping exists, even when that value is null. It returns the supplied default only when the key is absent.

12.6 containsKey()

Checks key presence and is the correct way to distinguish an absent key from a key mapped to null.

13. Concurrency, Iteration, and Choosing the Right Map

HashMap is not designed for unsynchronized concurrent mutation. Concurrent reads are safe only when the map is safely published and no thread modifies it. When writes may occur, use proper synchronization or a concurrent collection.

  • HashMap: fast, general-purpose, unordered, allows null key and null values.
  • LinkedHashMap: preserves insertion order, or access order when configured.
  • TreeMap: keeps keys sorted and normally provides O(log n) operations.
  • ConcurrentHashMap: designed for concurrent access and rejects null keys and null values.

HashMap iterators are fail-fast on a best-effort basis. Structural modification outside the iterator during iteration may throw ConcurrentModificationException. The exception is a bug detector, not a thread-safety mechanism.

Why ConcurrentHashMap Rejects null
A null result from get() must unambiguously mean “no mapping” during concurrent operations. Allowing null values would make that distinction unreliable while other threads are changing the map.

14. Expanded Doubts Reference

The attached article explains the core mechanics. The following section expands beyond that core with the full set of interview-style doubts raised in this conversation. The answers are intentionally compact so this section works as a quick revision sheet.

14.1 Keys and Object References

1. When I call put(key, value), does HashMap store the original key object or a copy? It stores the original object reference. HashMap does not clone or snapshot the key.

2. When I call get(anotherEqualKey), does the map replace the stored key? No. get() only searches. The equal object is used to locate the existing node and is not inserted.

3. If two objects are equals() == true, are they treated as the same key? Yes, provided their hash codes also obey the contract. A later put() updates the existing logical mapping.

4. If two objects have the same hashCode() but equals() is false, what happens? They are different keys in the same bucket. HashMap stores both and uses equals() during retrieval.

5. If equals() is true but hash codes are different, will retrieval work? It is not reliable. The objects may be routed to different buckets, violating the contract and causing missed lookups or duplicate logical keys.

6. When an equal key is inserted again, is the key object or only the value replaced? HashMap replaces the value associated with the existing node. In the current OpenJDK implementation, the original stored key reference normally remains.

7. Does HashMap compare keys using == or equals()? It can use both: identity is checked as a fast path, then equals() is used for logically equal but distinct objects.

8. Why compare the hash before equals()? An integer comparison is cheap. When hashes differ, the keys cannot be equal under a correct contract, so HashMap avoids an unnecessary equals() call.

14.2 Mutability and Custom Keys

9. What happens if I modify a key after inserting it? When the changed field affects equals() or hashCode(), later lookup may search the wrong bucket. The entry can remain in the map but become unreachable by normal key operations.

10. Is making fields final enough to make a key immutable? It prevents reassignment of the fields, but referenced mutable objects can still change. The equality state must be deeply stable.

11. Can a final List still make a key unsafe? Yes. final fixes the list reference, not its contents. Use List.copyOf(), an immutable collection, or exclude changing data from equality.

12. Why is String safe as a HashMap key? String is immutable, has content-based equality, and its hash remains stable throughout the object lifetime.

13. Can a record contain mutable objects and still be unsafe? Yes. Records are shallowly immutable. Generated equals() and hashCode() include mutable components unless you design around them.

14. What happens if only equals() is overridden but not hashCode()? Equal instances may retain identity-based hash codes and reach different buckets. Retrieval with an equal instance may fail.

15. Should every field be included in equals() and hashCode()? Include the fields that define logical key identity, and use the same set in both methods. Do not include fields that change independently of identity.

16. Can I use a database-generated ID before an entity is saved? Be careful. A null or temporary ID that later changes can alter equality and hash behavior. Persistent entities often need a carefully chosen stable business key or a lifecycle-aware equality strategy.

14.3 Null Behavior

17. Why does HashMap allow only one null key? Keys are unique. Every null key represents the same logical key, so another put(null, value) updates that mapping.

18. Why can multiple keys have null values? Values are not unique. Any number of distinct keys may map to the same value, including null.

19. How does HashMap calculate the bucket for a null key? The null key receives an internal hash of zero, which selects bucket index zero.

20. How do I know whether get() returned null because the key is absent? Call containsKey(). A true result means the key exists even when its associated value is null.

21. What happens when put(null, value) is called twice? The second call replaces the value for the single null-key mapping, and the size does not increase.

14.4 Collisions and Buckets

22. Can two different hash codes still go into the same bucket? Yes. The bucket index uses only the bits needed for the current table length, so different full hashes can map to the same index.

23. If two keys are in the same bucket, how is the correct one found? HashMap checks cached hashes and then identity or equals() while traversing the bucket list or tree.

24. Does a collision mean one entry overwrites another? No. Overwriting happens only when the keys are equal. Unequal colliding keys coexist.

25. When does a bucket change from a linked list to a red-black tree? In commonly discussed Java 8+ behavior, treeification is considered around eight nodes, provided the table capacity is at least 64.

26. Why require capacity 64 before treeification? For a small table, resizing is usually more useful because it may separate entries into different buckets. Tree nodes also consume more memory.

27. If every key returns the same hash code, will the map still work? It can remain functionally correct when equals() is correct, but all operations concentrate in one bucket and performance degrades.

28. Why does a weak hashCode() reduce performance? It creates uneven distribution, longer bucket searches, more treeification, and more comparisons per operation.

14.5 Resizing and Capacity

29. Why is the default capacity 16? It is a practical small power-of-two starting point that balances initial memory with room for common small maps.

30. What exactly does load factor 0.75 mean? The resize threshold is normally capacity multiplied by 0.75, balancing table memory against collision risk.

31. Does resizing happen when size equals the threshold? An insertion normally triggers resizing when the resulting size becomes greater than the threshold.

32. Are key hashCode() methods called again during resizing? Modern HashMap stores each node hash and uses it to split buckets. It does not need to call every key hashCode() again.

33. Why does capacity normally double? Doubling preserves the power-of-two invariant and lets each old bucket split into only two possible new positions.

34. Why must capacity be a power of two? It makes (length – 1) & hash an efficient index calculation and enables the resize split optimization.

35. Can a very large initial capacity hurt performance? Yes. It wastes memory and can make iteration slower because iteration scans the table capacity as well as the stored entries.

36. What happens to entries during resizing? A new table is created. Nodes are redistributed, normally either staying at the old index or moving by oldCapacity.

14.6 Retrieval and Update Behavior

37. Does get() scan the entire map? No. It calculates one bucket index and searches only that bucket structure.

38. What if the first node in the bucket is not equal to the key? HashMap follows the linked nodes or searches the tree until it finds an equal key or reaches the end.

39. Does calling get() modify the map? No. Ordinary get() is a read operation and does not insert or replace anything.

40. Does put() with an existing key increase the size? No. It replaces the value associated with that logical key, so size remains unchanged.

41. What does put() return when replacing a value? It returns the previous value. A null return is ambiguous because there may have been no mapping or the old value may have been null.

42. What is the difference between put(), putIfAbsent(), and computeIfAbsent()? put() always associates the value; putIfAbsent() preserves a present non-null value; computeIfAbsent() calculates a value when absent or mapped to null.

43. Can containsKey() and get() appear to disagree? Yes. A key mapped to null makes get() return null while containsKey() returns true.

44. What happens if equals() throws an exception during retrieval? The map operation propagates the exception. Key equality methods should be safe, deterministic, and free from fragile external dependencies.

14.7 Concurrency and Iteration

45. Is HashMap safe when multiple threads only read it? It can be safe after correct publication when no thread mutates it. Unsafe publication or concurrent writes invalidate that assumption.

46. What happens when one thread writes while another reads? Without synchronization, results are not reliably defined for application logic. Readers may observe stale or inconsistent state, and compound operations can race.

47. Why use ConcurrentHashMap instead of manually synchronizing every operation? It provides atomic concurrent-map operations and better concurrency than a single external lock for many workloads. Manual locking is still useful when several operations must share one larger critical section.

48. Why does ConcurrentHashMap reject null keys and null values? It keeps null from get() unambiguously meaning no mapping, which is important when other threads can change the map immediately.

49. What causes ConcurrentModificationException? A fail-fast iterator may detect structural modification performed outside that iterator after the iterator was created.

50. Does HashMap preserve insertion order? No. Any observed order is an implementation accident and can change after resizing or different insertion patterns.

51. Why can iteration order change after resizing? Nodes may move to new bucket positions, and iteration follows bucket order rather than insertion order.

52. What is the difference between HashMap, LinkedHashMap, and TreeMap? HashMap is unordered and expected O(1); LinkedHashMap maintains linked iteration order; TreeMap sorts keys and normally costs O(log n) per operation.

15. Practical Key-Design Checklist

  • Choose fields that represent stable logical identity.
  • Use the same identity fields in equals() and hashCode().
  • Make the class final, or use a record when inheritance is not required.
  • Make identity fields final and expose no mutation methods.
  • Use immutable component types or defensive copies.
  • Test two separate equal instances as put and get keys.
  • Test that equal instances have equal hash codes.
  • Never change equality state while the object is inside a hash-based collection.
  • Use containsKey() when null values are allowed and presence matters.
  • Use ConcurrentHashMap when concurrent mutation is part of the design.
EmployeeKey stored = new EmployeeKey(101, "OpenAI");
EmployeeKey lookup = new EmployeeKey(101, "OpenAI");
 
Map<EmployeeKey, String> map = new HashMap<>();
map.put(stored, "Alice");
 
assert stored != lookup;
assert stored.equals(lookup);
assert stored.hashCode() == lookup.hashCode();
assert "Alice".equals(map.get(lookup));
assert map.size() == 1;

17. Final Mental Model

Carry this picture into daily coding and interviews. HashMap stores the original key reference inside a node. hashCode() routes an operation to a bucket. equals() confirms the logical key. get() only searches. put() either creates a new node or updates the value of an equal existing key. Reassigning a variable does not modify the stored object, but mutating the equality state of the stored object can make the mapping unreachable.

Once those facts are clear, the confusing behaviors around String keys, records, final fields, nulls, collisions, and resizing stop feeling like separate rules. They become consequences of one consistent design.

18. Further Reading and Practice

  • Read the HashMap source methods hash(), putVal(), getNode(), resize(), and treeifyBin() with this mental model beside you.
  • Write tests for correct keys, broken hashCode(), constant hashCode(), and mutable keys.
  • Compare HashMap behavior with LinkedHashMap, TreeMap, IdentityHashMap, WeakHashMap, and ConcurrentHashMap.
  • Practice explaining why equal lookup objects work even though == returns false.
  • Practice explaining why one null key is a key-uniqueness rule rather than a bucket limitation.

Leave a Comment