A HashMap is like a row of mailboxes: the hash chooses the street, and equals() chooses the exact apartment. Interviewers love this question because it reveals whether you understand both the public API and the hidden rules that keep lookups fast.
Question: HashMap internal working.
Answer: HashMap stores key-value pairs in an array of buckets. It uses hashCode() to pick a bucket and equals() to find the exact key inside that bucket. In modern Java, collisions start as a short linked list and may become a red-black tree when a bucket gets too crowded, which keeps performance much better than a long linear scan.
Interview-Ready Answer: "I would explain that HashMap first turns a key into a hash, then maps that hash to an array bucket using a very fast bit operation. If two keys land in the same bucket, Java uses equals() to find or replace the right entry. In Java 8 and later, a heavily collided bucket can turn into a red-black tree, so average operations stay near O(1), and the worst case inside that bucket improves to about O(log n)."
HashMap is a key-value container backed by an array of buckets. A bucket is just a slot that can hold one or more entries. In Java 8+, each entry is a Node with a stored hash, key, value, and a link to the next node. HashMap does not keep keys sorted or in insertion order; its only goal is fast lookup by key.
put and get work under the hoodhashCode(). This can be any 32-bit integer, including a negative one.h ^ (h >>> 16)). This matters because bucket selection mostly uses the lower bits.(n - 1) & hash, where n is the table size. This works because the table size is always a power of two.equals(). If the key matches, the value is replaced; otherwise, the new node is added to that bucket.size crosses the threshold, HashMap resizes: the backing array grows, usually by doubling.Using a power-of-two length lets HashMap replace slow modulo with a fast bit mask. That is why (n - 1) & hash is used instead of hash % n. It is also why resize is efficient: when capacity doubles, each entry either stays in the same bucket or moves by exactly the old capacity.
Collisions are normal. Two different keys can produce the same bucket index, so HashMap must compare keys inside the bucket. In Java 8+, small collisions stay as a linked list because that is cheap and simple. When a bucket gets very crowded, treeification can happen, but only if the table is already large enough; otherwise HashMap prefers resizing first.
| Map type | Order | Best use | Lookup |
|---|---|---|---|
| HashMap | No order | Fast key access | Average O(1) |
| LinkedHashMap | Insertion order | Predictable iteration | Average O(1) |
| TreeMap | Sorted keys | Range queries | O(log n) |
Default initial capacity is 16, and default load factor is 0.75. That means the resize threshold is 12 entries at the default size, 24 at capacity 32, 48 at capacity 64, and so on. Resize is one of the most expensive operations because every entry must be examined and moved, so if you already know roughly how many items you will store, pre-sizing a HashMap can save time.
get, put, and remove: O(1).null key and many null values.get(key) returning null can mean either "missing key" or "present with null value", so use containsKey() when you need to distinguish the two.equals() and hashCode() do not follow the contract, lookups become unreliable.Use HashMap when you want fast lookup by key and do not need ordering or built-in thread safety. If multiple threads write to it, use external synchronization or ConcurrentHashMap.
Imagine a checkout service in an e-commerce system keeping orderId -> OrderState in a HashMap so it can answer requests quickly. Most of the time it is perfect: one lookup, one update, done. But one day an engineer uses a mutable object as the key and changes one of its fields after the object is put into the map.
What goes wrong is painful: the entry is still in the map, but it is now effectively unreachable because the key’s hash and equality result no longer match the original bucket. The symptoms are confusing: get() returns null, containsKey() fails, the service starts creating duplicate orders, and logs show increasing cache misses even though the map size keeps growing. If the issue is also combined with a poor hashCode(), one bucket becomes hot, CPU rises, and p99 latency spikes during peak traffic.
The lesson is simple: keys should be effectively immutable, and a good hashCode() matters because it spreads traffic across buckets instead of turning one bucket into a traffic jam.
import java.util.HashMap;
import java.util.Map;
public class HashMapInternalWorkingDemo {
// A key with a forced hash lets us create collisions on purpose.
// This shows that HashMap can still work correctly when multiple keys land in the same bucket.
private static final class Key {
private final int id;
private final int forcedHash;
private Key(int id, int forcedHash) {
this.id = id;
this.forcedHash = forcedHash;
}
@Override
public int hashCode() {
return forcedHash;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Key)) {
return false;
}
Key other = (Key) obj;
return this.id == other.id;
}
@Override
public String toString() {
return "Key{id=" + id + ", hash=" + forcedHash + "}";
}
}
public static void main(String[] args) {
// Small initial capacity makes resize easier to hit in a tiny demo.
// In real code, pre-size the map if you already know the approximate number of entries.
Map<Key, String> map = new HashMap<>(4, 0.75f);
Key k1 = new Key(1, 42);
Key k2 = new Key(2, 42);
Key k3 = new Key(3, 42);
map.put(k1, "one");
map.put(k2, "two");
map.put(k3, "three");
// Same logical key as k2: equals() says it is the same key,
// so the old value gets replaced instead of creating a duplicate entry.
map.put(new Key(2, 42), "TWO (updated)");
// HashMap allows one null key and many null values.
// A null value is a useful edge case because get() will also return null for a missing key.
map.put(null, "NULL-KEY");
map.put(new Key(4, 99), null);
System.out.println("size = " + map.size());
System.out.println("get(new Key(2,42)) = " + map.get(new Key(2, 42)));
System.out.println("get(missing key) = " + map.get(new Key(99, 42)));
System.out.println("get(null) = " + map.get(null));
System.out.println("get(new Key(4,99)) = " + map.get(new Key(4, 99)));
System.out.println("containsKey(new Key(4,99)) = " + map.containsKey(new Key(4, 99)));
System.out.println("containsKey(missing key) = " + map.containsKey(new Key(99, 42)));
System.out.println();
System.out.println("Iteration order is not guaranteed:");
for (Map.Entry<Key, String> entry : map.entrySet()) {
System.out.println(entry.getKey() + " -> " + entry.getValue());
}
}
}Follow-up & Tricky Questions:
equals() to find the exact key inside that bucket.ConcurrentHashMap or external locking.LinkedHashMap; if you need sorted keys, use TreeMap.hashCode() coexist? Yes. Same hash does not mean same key. If equals() says they are different, HashMap stores both entries in the same bucket.hashCode() break HashMap? No. Java mixes and masks the hash, so negative values are fine.get() returning null always mean the key is absent? No. The key may be present with a null value, so containsKey() is the safe check.Tricky / gotcha questions:
Common Mistakes:
hashCode() alone is enough. Correction: HashMap uses hashCode() to choose a bucket, then equals() to identify the exact key.LinkedHashMap if order matters.get() returning null as proof that the key is absent. Correction: a key can exist with a null value, so use containsKey() when needed.Memory Hook: Think of HashMap as a postal system: hashCode() picks the street, equals() picks the house, resize adds more streets, and treeification is what happens when one street gets too crowded.
Cheat Sheet:
get/put is O(1).hashCode() and equals().Practice Tasks:
hashCode() and observe how many collisions you create.HashMap and LinkedHashMap with the same inserts.