Hello! Welcome to the first lesson of our "Java Collections & Problem-Solving" module.
In the previous module, we laid a strong foundation in Core Java, culminating in a deep dive into the equals() and hashCode() contract. As we discussed, a proper implementation of these two methods is not just a matter of correctness—it's the absolute bedrock upon which Java's hash-based collections are built.
Today, we will see that principle in action as we explore one of the most fundamental and frequently asked-about topics in any senior Java interview: the internal workings of HashMap. By the end of this lesson, you will be able to explain exactly how HashMap achieves its impressive average O(1) performance for put and get operations by deconstructing its three core mechanics: hashing, collision handling, and resizing.
1. The Core Structure: Buckets and Nodes
At its heart, a HashMap is surprisingly simple: it's an array of "buckets". When you create a new HashMap(), Java allocates an array in memory with a default capacity of 16.
Each key-value pair you insert is wrapped in an object. Prior to Java 8, this was an Entry class. Since Java 8, it's called a Node. Each Node object holds four key pieces of information:
- The key's hash code (
int hash) - The key itself (
final K key) - The value (
V value) - A pointer to the next node in the same bucket (
Node<K,V> next)
This next pointer is the key to collision handling—it allows HashMap to form a linked list within a single bucket.

For a concise written overview of these components, you can refer to the first section of the article "How HashMap Works Internally After Java 8?".
How HashMap Works Internally After Java 8?
This article from Java Concept Of The Day provides a solid breakdown of the HashMap internals. Start by reading the first section to solidify your understanding of the basic components.
Read the section titled '1) Node<K, V> And table[]'. Focus on the structure of the Node class and how the table[] array holds these nodes.
2. The put() Operation: Hashing and Collision Handling
When you call map.put(key, value), HashMap performs a sequence of operations to place your data in the correct bucket. This process is the key to understanding HashMap.
To see this process in action, let's watch a video that walks through it step-by-step.
01. Internal Working of HashMap & Java-8 Enhancement
This video from Ankit Wasankar provides a clear, animated explanation of the put operation, including hash calculation, index determination, and what happens during a hash collision.
Watch the video from 04:11 to 10:24. Pay close attention to how it demonstrates: Calculating the hash code and bucket index. What a 'hash collision' is. How equals() is used to differentiate keys during a collision.
Let's break down that process in detail, as you would in an interview.
Step 1: Calculate the Key's HashHashMap first gets the hash code from the key by calling key.hashCode(). However, it doesn't use this value directly. To protect against poorly implemented hashCode() functions and to ensure better distribution, HashMap applies its own internal hash() function:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
The expression (h = key.hashCode()) ^ (h >>> 16) is a clever trick. It performs a bitwise XOR between the original hash code and the same hash code right-shifted by 16 bits. This effectively mixes the higher-order bits with the lower-order bits, spreading out the hash values more evenly and reducing the chance of collisions.
Step 2: Calculate the Bucket Index
Next, HashMap must map this hash value to an index in its internal array. It does this with another efficient bitwise operation:int index = (n - 1) & hash;
Here, n is the capacity of the array (e.g., 16). Because the capacity is always a power of two, (n-1) results in a bitmask (e.g., 15 is 00001111 in binary). The & operation is a very fast way to compute the remainder (like hash % n), ensuring the index is always within the array's bounds.
Step 3: Handle the BucketHashMap now has the target bucket index.
-
If the bucket is empty: It creates a new
Nodeand places it at that index. The operation is complete. -
If the bucket is NOT empty (a collision): The
HashMapmust check if it's adding a new key or updating an existing one. It traverses the linked list at that bucket, and for eachNodeit finds, it performs two checks:- Does the new key's hash match the existing node's hash?
- If the hashes match, does
key.equals(existingNode.key)returntrue?
If both are true, it means the key already exists.
HashMapupdates the value in that node and the operation is done. If it reaches the end of the list without finding a match, it appends the newNodeto the end of the linked list.
3. Java 8 Enhancement: Treeification
In older versions of Java, a large number of collisions in one bucket could degrade performance significantly. If many keys mapped to the same bucket, the linked list would become very long. Retrieving an element from this long list would take O(n) time, defeating the purpose of a HashMap.
Java 8 introduced a major optimization called treeification.
- If the number of nodes in a single bucket reaches a certain threshold (specifically,
TREEIFY_THRESHOLD, which defaults to 8), the linked list is converted into a self-balancing Red-Black Tree.
This dramatically improves worst-case performance. Searching for an element in a balanced tree takes O(log n) time, which is a massive improvement over the O(n) of a long linked list.
This conversion only happens if the map's total capacity is also large enough (at least MIN_TREEIFY_CAPACITY, which is 64). If the map is still small, it will prioritize resizing the whole map over treeifying a single bucket.
4. Resizing: How a HashMap Grows
A HashMap can't just keep adding nodes to its buckets indefinitely. As the map fills up, the probability of collisions increases. To maintain its performance, the HashMap resizes itself.
Resizing is triggered based on two parameters:
- Capacity: The current length of the internal array (e.g., 16).
- Load Factor: A measure of how full the map is allowed to get before it's resized. The default is 0.75.
The threshold for resizing is calculated as capacity * loadFactor.
For a default HashMap, the threshold is 16 * 0.75 = 12. This means when you add the 13th element, the HashMap will trigger a resize.
The Resizing Process:
- A new array is created with double the capacity of the old one (e.g., 16 -> 32).
- All existing entries from the old array are iterated over and re-hashed into the new, larger array. Their bucket index is recalculated based on the new capacity.
This rehashing process is computationally expensive, which is why it's a good performance practice to initialize your HashMap with an appropriate capacity if you know roughly how many elements it will hold.
// Good practice if you expect ~1000 items
Map<String, User> userMap = new HashMap<>(1500); // Prevents multiple resizes
Test your understanding!
An interviewer asks: "You are using Java 8 or later. You insert 10 distinct keys into a HashMap, but due to a very poor hashCode() implementation, all 10 keys produce the same hash code and map to the same bucket. Describe the internal structure of that bucket after all 10 insertions. What is the time complexity to retrieve the 8th element you inserted?"
Show answer
Answer:
-
Internal Structure:
- After the 8th element is inserted into the bucket, the number of nodes in that bucket's linked list reaches the
TREEIFY_THRESHOLDof 8. - At this point, the
HashMapconverts the linked list into a Red-Black Tree. - The 9th and 10th elements are then inserted directly into this tree structure.
- Therefore, after 10 insertions, the bucket will contain a Red-Black Tree with 10 nodes.
- After the 8th element is inserted into the bucket, the number of nodes in that bucket's linked list reaches the
-
Time Complexity:
- Because the structure is now a balanced binary tree, the time complexity to retrieve any element (including the 8th one inserted) is O(log n), where n is the number of nodes in that bucket. In this case, it would be O(log 10). This is a significant improvement over the O(n) time it would have taken if it remained a linked list.
5. hashCode() and equals() in Action
To truly grasp when hashCode() and equals() are called, there's no substitute for seeing it live. The following video provides a brilliant coding demonstration that's perfect for interview preparation.
How does a HashMap Work Internally? | Core Java Interview Question 12
Cameron McKenzie provides an excellent coding example in this video. He creates a custom key class and overrides hashCode() and equals() to print messages, showing you exactly when each method is invoked during put and get operations, especially during collisions.
Watch the live coding demonstration from 17:25 to 26:01. First, observe the 'happy path' where all hash codes are unique. Then, pay close attention to what happens when he forces all keys to have the same hash code. Notice how many equals() calls are suddenly required.
This demonstration makes it crystal clear:
hashCode()is called on everyputandgetto find the bucket.equals()is only called when a collision occurs—that is, whenHashMapfinds an existing node in the target bucket with the same hash.
Conclusion
You have now deconstructed one of the most important data structures in the Java ecosystem. Understanding HashMap internals is a hallmark of a proficient Java developer and a common expectation in demanding technical interviews.
Key Takeaways:
- Structure: A
HashMapis an array of buckets, where each bucket holds nodes in a linked list or a Red-Black Tree. - Hashing: The
put(key, value)operation calculates a bucket index usingkey.hashCode(), an internalhash()function, and a bitwise&operation with the array's capacity. - Collisions: When multiple keys map to the same bucket, their nodes are stored together. The
equals()method is used to differentiate them. - Performance: For a well-distributed set of keys, operations are O(1) on average. In the worst-case scenario of many collisions, Java 8's treeification ensures O(log n) performance.
- Resizing: The map automatically doubles its capacity and rehashes all entries when its size exceeds the
capacity * loadFactorthreshold.
In our next lesson, we will build on this knowledge to compare and contrast HashMap, LinkedHashMap, TreeMap, and ConcurrentHashMap. We'll analyze their specific use cases, performance trade-offs, and how they differ in ordering and thread-safety.