How does HashMap work internally, and what happens when it resizes?
HashMap stores entries in an array of buckets indexed by a hash of the key; collisions chain into a linked list that converts to a red-black tree past a threshold, and exceeding the load factor triggers a resize that reallocates and redistributes every entry.
What the interviewer is scoring
- Whether you know the bucket-array-plus-collision-chain structure, not just the public API
- Whether you can state the equals and hashCode contract and why violating it breaks lookups
- Whether you understand that resize is O(n) and therefore why initial capacity matters
- Whether you volunteer thread-safety limitations without being asked
Answer
The structure
A HashMap holds an array of buckets, referred to internally as the table. Inserting a key computes hashCode(), applies a supplemental spreading function that XORs the high bits into the low bits, and takes the low bits of the result to pick a bucket index. That spreading step exists because the index is derived with a bitmask rather than a modulo, so without it two keys differing only in their high bits would collide constantly.
When two keys land in the same bucket, the entries are chained. Up to eight entries in one bucket the chain is a singly linked list, and lookup within it degrades to a linear scan. Past that threshold, and provided the table is at least 64 slots, the chain converts into a red-black tree so worst-case lookup for that bucket becomes O(log n) rather than O(n). This treeification was added in Java 8 specifically to blunt hash-collision denial-of-service attacks, where an attacker submits keys engineered to collide.
The equals and hashCode contract
Lookup is a two-step process: find the bucket by hash, then walk the chain comparing with equals(). Both steps must agree for the map to work. If two objects are equals() but return different hash codes, they will usually be placed in different buckets and the map will contain what looks like a duplicate key. If the hash codes agree but equals() does not, you will insert a value and never retrieve it.
This is also why mutating a key after insertion is a genuine bug rather than a stylistic concern. The entry stays in the bucket determined by the old hash, but lookups now compute the new hash and search the wrong bucket, so the entry becomes unreachable while still consuming memory.
Resizing
The map tracks a threshold equal to capacity times the load factor, 0.75 by default. Exceeding it doubles the table and redistributes every existing entry, which is an O(n) operation. Java 8 improved the mechanics: because capacity is always a power of two and the index is a bitmask, an entry either stays at its current index or moves to index + oldCapacity, so a chain can be split into two without rehashing each key.
The practical consequence is that a map you know will hold ten thousand entries should be constructed with an explicit initial capacity. Left at the default it will resize roughly ten times on the way there, and each resize touches every entry inserted so far.
// Repeatedly resizes: 16 -> 32 -> 64 -> ... while filling.
Map<String, User> lazy = new HashMap<>();
// Sized once. The /0.75f accounts for the load factor so the
// threshold is not hit at exactly the expected element count.
Map<String, User> sized = new HashMap<>((int) (10_000 / 0.75f) + 1);
Thread safety
HashMap is not synchronised, and the failure mode is worse than lost updates. Concurrent writes during a resize could, before Java 8, produce a circular chain that made a subsequent get() spin forever. Java 8 removed that specific cycle, but concurrent modification is still unsafe and can lose entries or throw ConcurrentModificationException during iteration.
Mentioning this unprompted is worth real credit, because it signals you think about the class in a production context rather than as a textbook data structure. The correct answer for concurrent use is ConcurrentHashMap, which locks per-bin rather than wrapping the whole map in a single lock the way Collections.synchronizedMap does.
Likely follow-ups
- Why is the default capacity 16 and the load factor 0.75?
- What breaks if a key object is mutated after being inserted?
- How does ConcurrentHashMap achieve thread safety without locking the whole map?
- Why did Java 8 introduce treeification, and what problem did it solve?
Related questions
- Count the subarrays whose elements sum to k, where the values may be negative.mediumAlso on hashing4 min
- Find the length of the longest substring without repeating characters.mediumAlso on hashing2 min
- How do you split a story that is too big to fit in a Sprint?mediumSame kind of round: concept3 min
- What is the Definition of Done, who owns it, and what do you do with an item that misses it at the end of a Sprint?mediumSame kind of round: concept3 min
- Where does encapsulation or polymorphism change a design, and how do you decide between composition and inheritance?mediumSame kind of round: concept6 min
- What are the four pillars of OOP, and what is the difference between abstraction and encapsulation?easySame kind of round: concept4 min
- Take this table to third normal form, then tell me when you would deliberately denormalise — and what does ACID guarantee?mediumSame kind of round: concept5 min
- Walk me through Scrum as the Guide defines it: the accountabilities, the events, the artefacts, and what each event is for.easySame kind of round: concept5 min