Table of Contents

HashMap Internal Working in Java: A Beginner-Friendly Deep Dive

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

HashMap Internal Working in Java: A Beginner-Friendly Deep Dive

Learn the HashMap internal working in Java in simple words — hashing, buckets, collisions, resizing, and the equals/hashCode contract, with clear examples.

Almost every Java program you write ends up using a HashMap somewhere. It is that common. Yet most developers use it for years without ever asking how it works inside. This guide fixes that. We will walk through the HashMap internal working in Java one step at a time, in plain language, so the whole thing finally clicks.

At its heart, a HashMap is just a container for key-value pairs. You hand it a key, and it gives you back the value tied to that key. A phone contact list is a good mental picture. The contact name is the key, and the phone number is the value. You do not scroll through every contact to find one number. You jump straight to it. A HashMap gives your code that same jump-straight-to-it speed.

Before we open the hood, here are the headline facts. Keep them nearby, because the rest of the article keeps circling back to them.

  • A HashMap holds key-value pairs, and no two keys can be the same.
  • On average, adding, fetching, and deleting all take O(1) time, which feels instant.
  • It is not built for multiple threads, and it accepts one null key plus any number of null values.

1. What a HashMap Really Is

The HashMap class ships with Java inside the java.util package, as part of the Collections Framework. Reach for it any time you want to look something up by a key rather than a position. A few everyday examples make this concrete.

  • Linking an order ID to the full order details.
  • Mapping a country code like IN or US to the country name.
  • Remembering how many times each visitor hit a web page.

So what makes it special? Speed, mostly. Suppose you keep a plain list of a million entries and search it for one item. In the worst case, you touch every single element before you find a match. A HashMap skips that pain. It calculates where the item should live and goes there directly. That calculation is the secret sauce, and it has a name: hashing.

Here is a small addition worth mentioning early. Because a HashMap trades a little extra memory for that direct-access speed, it is a classic space-versus-time deal. You spend some memory on the bucket array so that every lookup stays cheap. In most applications that trade is well worth it.

2. The Building Blocks: Buckets and Nodes

Every HashMap wraps an internal array. Each slot in that array is called a bucket. When you look at the source, the array is declared roughly like this.

Node<K, V>[] table;

Each pair you store gets wrapped in a small object called a Node. A single Node carries four things, shown in this trimmed-down version of the real class.

static class Node<K, V> {
    final int hash;   // cached hash of the key
    final K key;      // the key itself
    V value;          // the value tied to the key
    Node<K, V> next;  // link to the next node in the bucket
}

Let us take those fields one by one, because they explain the map’s whole behavior.

  • hash: the number worked out from the key. It picks which bucket the node lands in.
  • key: the actual key object you passed in, such as a String or an Integer.
  • value: whatever value you paired with that key.
  • next: a link to the following node in the same bucket, used only when keys share a bucket.

That final field is the important clue. Since a node can point to a next node, one bucket is able to hold several nodes chained together. We will see exactly when and why that chaining kicks in a little later.

3. Hashing and Working Out the Index

When you insert a pair, the map first needs a number for the key. So it calls the key’s hashCode() method. That gives back an int. But an int on its own is not a valid array slot, so the map converts it into an index that fits inside the bucket array.

The conversion looks like this.

index = hash & (n - 1);

In that line, n is the current number of buckets, which starts at 16. The bitwise AND keeps the result inside the array bounds and helps scatter keys across the buckets. Spreading keys out matters, because clustered keys cause slow lookups.

Interview Insight
A favorite question: why does HashMap use hash & (n – 1) rather than hash % n? Two reasons. Bitwise AND runs faster than modulo, and it behaves like modulo only because capacity is always a power of two. That is also why HashMap rounds any capacity you request up to the next power of two.

Creating an empty map sets that starting capacity for you automatically.

HashMap<String, Integer> scores = new HashMap<>();
// Starts life with 16 buckets under the hood.

One extra detail rounds out this section. The map does not feed the raw hash straight into the formula. First it stirs the higher bits of the hash down into the lower bits. Java calls this the hash spread or perturbation step.

Why do that? The index formula only reads the lowest bits of the hash. If two keys differ mainly in their top bits, they would still collide. Mixing the bits first spreads keys more evenly, which keeps buckets short and lookups quick. It is a tiny trick with a real payoff.

4. How put() Works, Step by Step

The put() method drops a key-value pair into the map. It always follows the same routine. To see it clearly, we will add three entries, and the third one will deliberately clash with the first. This time we will use city names as keys.

4.1 Adding the First Entry

scores.put("Delhi", 20);

Behind the scenes, the map runs through these steps.

  • It asks “Delhi” for its hash code. Let us say that comes back as 118.
  • It runs the index formula on that hash and lands on bucket 6.
  • It builds a fresh Node holding the hash, key, value, and a null next link.
{
  int hash = 118;
  String key = "Delhi";
  Integer value = 20;
  Node next = null;
}

Bucket 6 is empty at this moment, so the node simply moves in. Nothing else needs to happen.

4.2 Adding the Second Entry

scores.put("Mumbai", 30);

The routine repeats, only the numbers change.

  • The hash of “Mumbai” comes back as 115, say.
  • This time the formula points to bucket 3.
  • A new node is created for the pair.
{
  int hash = 115;
  String key = "Mumbai";
  Integer value = 30;
  Node next = null;
}

Bucket 3 sits empty, so the node settles there cleanly. Still no drama.

4.3 Adding the Third Entry (A Collision)

scores.put("Chennai", 40);

Here comes the twist. Notice what the hash turns out to be.

  • The hash of “Chennai” also lands on 118, the same as Delhi.
  • So the formula again points to bucket 6.
  • A new node gets created for this pair.
{
  int hash = 118;
  String key = "Chennai";
  Integer value = 40;
  Node next = null;
}

But bucket 6 is already taken by the Delhi node. Two different keys now want the exact same slot. That situation is a collision. It sounds scary, yet the map takes it in stride.

When a collision shows up, the map compares the new key with the one already sitting there.

  • If the keys are truly equal (same hashCode() and equals() returns true), the map treats them as one key and just overwrites the old value.
  • If they are different keys that happened to share a bucket, the map links the new node onto the existing one through the next pointer.
HashMap collision handling in Java

5. How get() Works

The get() method fetches a value using its key. It walks almost the same path as put(). Let us trace a lookup for the key “Mumbai”.

Integer v = scores.get("Mumbai");

The steps stay short and predictable.

  • The map hashes “Mumbai” and gets 115 back.
  • The formula sends it straight to bucket 3.
  • It checks the key stored at bucket 3. If it matches, it returns the value. If not, it follows the next link and checks the node after it.

This is exactly why lookups feel instant. The map never sweeps the entire array. It heads to one bucket and inspects only the handful of nodes parked there.

There is a nice speed trick hidden in step three. Before running the fairly expensive equals() check, the map first compares the cached hash values, which are just ints. Comparing two ints is dirt cheap. So when the hashes differ, the map skips equals() entirely. Multiply that saving across millions of lookups and it adds up fast.

Interview Insight
You will often be asked why get() needs both hashCode() and equals(). Short answer: hashCode() finds the right bucket, but a bucket can hold many keys. So equals() is what confirms which key inside that bucket is actually yours.

6. Collisions, Explained Simply

A collision just means two different keys ended up pointing at the same bucket. It is not a bug and not rare. No hashing scheme can dodge collisions forever, so Java expects them and handles them cleanly.

When a collision happens, both entries live in the same bucket, linked together. How they are linked has evolved across Java versions, and that evolution is worth knowing.

6.1 Before Java 8

Older Java kept each bucket as a plain linked list. Every new colliding node was tacked onto the chain through its next reference. To find a key, the map walked that chain node by node until it hit a match or ran out.

6.2 From Java 8 Onward

Java 8 brought a clever improvement. Once a single bucket collects more than 8 nodes, its linked list is upgraded into a balanced tree made of TreeNode objects. Searching that tree costs O(log n) instead of the O(n) a long list would cost.

Two extra rules are worth adding here, since interviewers love them. First, the tree conversion only kicks in when the whole map has at least 64 buckets. If the map is still smaller than that, Java just resizes the array instead of building a tree. Second, trees are not permanent. If enough entries get removed and a tree bucket shrinks below 6 nodes, Java quietly converts it back into a linked list.

So collisions are never fatal. The map simply parks several entries in one bucket and picks whichever structure keeps lookups fast. Your code carries on none the wiser.

7. Resizing and Rehashing: How the Map Grows

A HashMap does not keep the same capacity forever. As entries pile up, buckets get crowded, collisions climb, and speed drops. To stay fast, the map enlarges itself when it gets too full.

7.1 When Does Resizing Trigger?

The trigger is the load factor, a number that decides how full the map may get before growing. The rule is easy to remember.

entries > capacity x loadFactor

Drop in the default values and it becomes obvious.

  • Capacity = 16
  • Load factor = 0.75
  • Threshold = 16 x 0.75 = 12

So the moment you add the 13th entry, the map decides it is time to grow. It deliberately does not wait until every bucket is packed.

7.2 What Happens During a Resize?

The growth itself is simple in concept, even if it takes some work.

  • A brand new array is allocated at double the old capacity, so 16 becomes 32.
  • Every existing entry is moved across into the bigger array.
  • A key’s index can change, because the capacity inside the index formula changed.

That whole move-and-recompute pass is called rehashing. It runs in O(n) time, so it is not cheap. That is precisely why the map only does it occasionally, instead of nudging the array on every insert.

7.3 The Java 8 Split Optimization

Modern Java made resizing lighter. When capacity doubles from N to 2N, each node in a bucket does one of only two things.

  • It stays put at the same index, or
  • It shifts to a new index equal to oldIndex + oldCapacity.

Because of this pattern, Java can split each bucket without recomputing full hashes for every key. The result is a faster, cheaper resize. It is a small piece of engineering with a genuinely nice return.

Interview Insight
Expect to be asked why the default load factor is 0.75. It is a deliberate compromise. A smaller value means fewer collisions but wasted memory, while a larger value packs the map tighter but invites collisions. The value 0.75 has proven to be a sweet spot for typical workloads.

8. How remove() Works

The remove() method deletes an entry by its key. It behaves a lot like get(), except it also repairs the bucket after pulling a node out. Three steps cover it.

8.1 Find the Bucket

First, the map hashes the key and computes its bucket index, exactly as get() does. That lands it on the correct bucket right away.

8.2 Search the Bucket

Next, it scans the nodes in that bucket, whether they form a list or a tree, hunting for a node whose key matches the one you asked to remove.

8.3 Detach the Node

Once it finds the match, it unhooks that node from the structure and fixes the surrounding links or tree pointers so nothing breaks. Then it decreases the size counter by one.

In a linked-list bucket, unhooking simply means pointing the previous node straight at the next node. The removed node is left dangling and later swept up by the garbage collector.

9. Why equals() and hashCode() Matter So Much

A HashMap leans hard on two methods that belong to your key object: hashCode() and equals(). Get them wrong and the map misbehaves in ways that are maddening to debug. So it is worth understanding the rules they must follow.

9.1 The Job of hashCode()

hashCode() hands back a number for the key, and that number chooses the bucket. A well-behaved hashCode() sticks to a few simple promises.

  • Stay consistent: the same key must return the same number for as long as it lives in the map.
  • Spread out: values should scatter well so entries do not all crowd into one bucket.
  • Match equals(): any two keys that are equal must produce identical hash codes.

9.2 The Job of equals()

equals() decides whether two keys are genuinely the same. When several keys share a bucket, the map calls equals() to pick the right one. Without a correct equals(), the map cannot tell your key apart from its neighbors.

9.3 A Correct Example

Here is a small key class that plays by the rules. Notice that both methods rely on the same field, which keeps them in sync.

public final class ProductKey {
    private final int sku;
 
    public ProductKey(int sku) {
        this.sku = sku;
    }
 
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof ProductKey)) return false;
        return sku == ((ProductKey) o).sku;
    }
 
    @Override
    public int hashCode() {
        return Integer.hashCode(sku);
    }
}

The class is final and the sku field is final too, so once you build a key it can never change. Both methods use only that field. As a result, equal keys always share a hash code, exactly as the contract demands.

9.4 Why This Really Matters

Now imagine a broken hashCode() or equals(). Or picture using a changeable object as a key and then editing a field that those methods depend on. Either slip can corrupt the map in confusing ways.

  • You store an entry, then get() cannot find it afterward.
  • You end up with duplicate keys that were supposed to be a single key.
  • You create surprise collisions that quietly slow everything down.

So correct hashCode() and equals() methods are not a nice-to-have. They are the foundation the whole map stands on.

Interview Insight
A classic trap: what breaks if you override equals() but forget hashCode()? Two objects you consider equal may hash to different buckets. The map then stores both as separate keys, and a later get() can miss the entry you expected to find.

10. Time Complexity at a Glance

Let us pin down how fast these operations really are. The honest answer is that speed depends on how evenly your keys spread across buckets.

10.1 The Average Case

When keys spread out nicely, the numbers look great.

put:    O(1)
get:    O(1)
remove: O(1)

That near-instant behavior is the whole reason developers reach for a HashMap first whenever fast lookups matter.

10.2 The Worst Case

Now imagine a flood of keys all crashing into the same bucket, whether through weak hashing or a deliberate attack. Performance then hinges on your Java version.

  • Older Java keeps that bucket as a linked list, so operations sink to O(n).
  • Java 8 and later grow a crowded bucket into a balanced tree, holding the cost to O(log n).

Clearly, modern Java copes with the worst case far more gracefully. The tree upgrade shields your program even under a punishing pile-up of collisions.

11. Handy Behaviors You Should Know

A HashMap carries a few extra traits that surface often in real code and in interviews. Let us run through them.

11.1 Null Keys and Null Values

The map is relaxed about nulls, within limits.

  • It permits exactly one null key.
  • It permits any number of null values.
  • The null key is handled as a special case and usually parked in a fixed bucket.

11.2 Iteration Order

A HashMap makes no promise about the order you see when looping over it. Small tests might look stable, but you must not depend on that. A resize or a different insertion pattern can reshuffle everything. When order truly matters, switch to a LinkedHashMap instead.

11.3 Fail-Fast Iterators

The iterators of a HashMap are fail-fast. If you change the map’s structure mid-loop, and you do not go through the iterator’s own remove method, it throws a ConcurrentModificationException. Think of it as a smoke alarm. It stops you from chasing silent, unpredictable bugs during iteration.

11.4 Not Thread-Safe

A HashMap is not synchronized, so sharing one across threads without protection is asking for trouble. When several threads need the same map, you have better tools.

  • Use ConcurrentHashMap for thread-safe access that avoids locking the whole map.
  • Or wrap and synchronize the map yourself if you genuinely have to.

12. Where HashMaps Show Up in Real Projects

Once you know the pattern, you will spot HashMaps everywhere. Their flexible key-to-value mapping fits a surprising range of jobs. Here are the common ones.

12.1 Caches and Lookups

  • Caching database results so repeated calls stay cheap.
  • Mapping a user ID to that user’s full profile.
  • Holding runtime configuration values for quick access.

12.2 Indexing and Grouping

  • Grouping records under a shared category.
  • Turning cryptic error codes into readable messages.
  • Building lightweight in-memory indexes.

12.3 Frequency Counting

  • Tallying how often each word appears in a block of text.
  • Counting events triggered by each user.
  • Rolling up totals grouped by a key.

Frequency counting is such a common beginner task that it deserves a quick snippet. This one counts words, using getOrDefault to stay short and readable.

Map<String, Integer> counts = new HashMap<>();
for (String word : words) {
    counts.put(word, counts.getOrDefault(word, 0) + 1);
}

The word is the key and its running tally is the value. The map quietly handles all the lookups. You will meet this exact pattern in log analysis, search tooling, and countless small utilities.

12.4 Graphs and Richer Structures

  • Storing adjacency lists that map each node to its neighbors.
  • Attaching metadata maps to objects.
  • Flipping relationships around, such as mapping a value back to the keys that point to it.

13. Common Pitfalls and Best Practices

A handful of habits will keep your maps both correct and fast. Let us cover the ones that matter most.

13.1 Never Mutate a Key

Once a key is inside the map, leave it alone. If you change a field that hashCode() or equals() reads, the map can lose the entry entirely. Prefer immutable keys such as String, the wrapper types, or your own carefully designed immutable classes.

Picture the bug in plain terms. You insert a key that hashes into bucket 6. Later you mutate a field inside it, and now the same key would hash into bucket 10. When you call get(), the map looks in bucket 10 and finds nothing, even though your entry is still sitting quietly in bucket 6. The map simply cannot reach it anymore.

13.2 Write hashCode() and equals() Together

For any custom key class, treat these two methods as a pair. Include every field that defines equality in both. And keep them consistent: if equals() says two objects are equal, their hash codes must match too.

13.3 Size the Map Up Front

If you already know roughly how many entries are coming, pass an initial capacity when you create the map. Doing so avoids early resizes and the rehashing cost they bring. In hot code paths, this small step can make a real difference.

13.4 Respect the Load Factor

The default load factor of 0.75 is a safe pick for most situations. Even so, it helps to know the trade-off you are accepting.

  • A lower load factor means fewer collisions but more memory used.
  • A higher load factor means a denser map but more collisions to wade through.

13.5 Pick the Right Map for the Job

A HashMap is not always the best answer. Match the map to the need.

  • HashMap: fast and unordered, the default workhorse.
  • LinkedHashMap: preserves the order you inserted things in.
  • TreeMap: keeps keys sorted, at a log-time cost per operation.
  • ConcurrentHashMap: thread-safe without locking the entire map.

14. HashMap Compared With Its Cousins

Beginners often blur the different map classes together. Each one exists for a slightly different reason. A few quick side-by-sides clear the fog.

14.1 HashMap vs Hashtable

Hashtable is a leftover from early Java. Every one of its methods is synchronized, which locks the whole table on each call. That makes it thread-safe but sluggish, and it refuses null keys and null values outright. HashMap, by contrast, skips synchronization, runs faster in single-threaded code, and tolerates nulls. For new work, most teams ignore Hashtable and reach for ConcurrentHashMap when they need safety.

14.2 HashMap vs LinkedHashMap

LinkedHashMap builds directly on HashMap and adds a linked list that remembers insertion order. So iterating gives you entries back in the order you added them. It shines when order carries meaning, such as an LRU-style cache that should drop its oldest entry first.

14.3 HashMap vs TreeMap

TreeMap keeps its keys sorted at all times using a red-black tree. That sorting costs you O(log n) per operation instead of O(1). Choose it when you need keys in order or want range queries, and accept the modest speed hit in exchange.

14.4 HashMap vs ConcurrentHashMap

ConcurrentHashMap is the thread-safe relative of HashMap. Rather than locking the whole map on every write, it locks only small segments, so many threads can work at once. It is the right tool for multi-threaded services where several threads read and write together.

15. A Few Doubts, Cleared Up

Certain questions come up again and again for newcomers. Let us settle them so they never slow you down.

15.1 Does a HashMap keep any order?

No. Whatever order you see while iterating is not guaranteed and can shift after a resize. If order matters to you, use a LinkedHashMap or a TreeMap instead.

15.2 Can two keys share a bucket forever?

Yes, and that is completely fine. Different keys can happily coexist in one bucket through chaining. The map still tells them apart with equals(). Nothing breaks as long as your key methods are correct.

15.3 Is a huge initial capacity always smart?

Not really. A large capacity burns memory even while the map sits mostly empty. Set it high only when you truly expect many entries. For small maps, the default of 16 is perfectly fine.

15.4 Why must capacity be a power of two?

Because the index formula uses hash & (n – 1). That AND trick only distributes keys evenly when n is a power of two. So Java always rounds any requested capacity up to the next power of two.

16. One Mental Model to Tie It Together

Let us wrap the whole class into a single picture you can carry into any interview. Whenever you think about a HashMap, imagine three parts working as a team.

  • The hash step turns a key into a number, then into a bucket index.
  • The bucket holds one or more nodes, arranged as a list or a tree.
  • The equals() check confirms the exact key inside a shared bucket.

Every operation reuses those three parts. A put() call locates the bucket and either adds or replaces a node. Meanwhile, get() locates the bucket and returns the matching value. Finally, remove() locates the bucket and detaches the node. Once you see this rhythm, the entire class feels predictable.

Hold onto this model during interviews. If you can explain those three parts with confidence, you can reason through almost any HashMap question thrown at you. Resizing, treeification, and the rest are just extra layers stacked on top of this simple core.

17. Interview Questions

Q: How does HashMap work internally in Java?

A: A HashMap stores entries in an array of buckets. It calls hashCode() on the key, converts that hash into a bucket index using index = hash & (n-1), and places the node in that bucket. Lookups jump straight to the bucket instead of scanning every entry, which gives average O(1) speed.

Q: What happens during a collision in a HashMap?

A: A collision happens when two different keys map to the same bucket. The map stores both entries in that bucket and links them. Before Java 8 it used a linked list. Since Java 8, a bucket with more than 8 nodes (in a map of at least 64 buckets) converts to a balanced tree for O(log n) lookups.

Q: When does a HashMap resize?

A: A HashMap resizes when the number of entries exceeds capacity multiplied by the load factor. With the default capacity of 16 and load factor of 0.75, the threshold is 12, so the 13th entry triggers a resize. The array doubles and entries are rehashed into the new buckets.

Q: Why are equals() and hashCode() important for HashMap keys?

A: hashCode() decides which bucket a key goes into, and equals() confirms the exact key inside a crowded bucket. If two equal keys return different hash codes, the map may store duplicates or fail to find entries. So both methods must agree, and keys should be immutable.

Q: Is HashMap thread-safe?

A: No. HashMap is not synchronized and is unsafe for concurrent use without external synchronization. For thread-safe access, use ConcurrentHashMap, which locks only small parts of the map instead of the whole structure.

Q: What is the time complexity of HashMap operations?

A: For well-distributed keys, put, get, and remove are O(1) on average. In the worst case with many collisions, older Java versions drop to O(n) with linked lists, while Java 8+ stays at O(log n) using balanced trees.

18. Conclusion

You now have a solid grip on the HashMap internal working in Java. We traced how a key becomes a hash, how that hash picks a bucket, and how collisions get absorbed through chains and trees. Along the way you saw how the map resizes itself, how remove() untangles a node, and why hashCode() and equals() sit at the very center of it all.

Carry these ideas into your everyday code. Favor immutable keys, size your maps with intent, and pick the map type that fits the task. With that foundation, you can wield a HashMap with real confidence and explain it clearly whenever an interviewer asks.

Leave a Comment