DEV Community

Anant Ghadi
Anant Ghadi

Posted on

How HashMap Really Works in Java 21: 7 Interview Follow-ups Most People Miss

"How does HashMap work internally?" is probably the most asked Java interview question. Most candidates say "it uses hashing and buckets" and stop there. The interviewer then asks a follow-up, and that is where the round is won or lost.

Here are the 7 follow-ups I see most often, with answers you can say out loud.

1. What actually happens when you call put()?

HashMap keeps an array of buckets (a Node<K,V>[] table). On put(key, value):

  • It calls key.hashCode() and spreads the bits: hash = h ^ (h >>> 16). This mixes the high bits into the low bits so small tables still get a good spread.
  • It finds the bucket with index = (n - 1) & hash, where n is the table size (always a power of two).
  • If the bucket is empty, the new node goes there. If not, it walks the bucket and uses equals() to check whether the key already exists. If it does, the value is replaced. If not, the node is added at the end.

One-line answer: hashCode() picks the bucket, equals() picks the entry.

2. What are the default capacity and load factor, and why do they matter?

Default capacity is 16 and the load factor is 0.75, so the map resizes once it holds more than 12 entries. Resizing doubles the table and redistributes the entries, which costs time. If you know you will store 10,000 entries, size the map up front.

Since Java 19 there is a cleaner way:

Map<String, Order> orders = HashMap.newHashMap(10_000);
Enter fullscreen mode Exit fullscreen mode

This sizes the table for 10,000 mappings with the load factor already accounted for. Most candidates have never heard of it, so mentioning it stands out.

3. How does resizing work without recalculating every hash?

Because the table size is a power of two, doubling it adds one more bit to the index. Java 8+ checks (hash & oldCapacity). If that bit is 0, the entry stays at the same index. If it is 1, it moves to index + oldCapacity. Each bucket splits into a "low" and a "high" list, and the original order is kept.

4. What happens when many keys collide in one bucket?

Up to Java 7, a bucket was always a linked list, so a bad hashCode() could turn lookups into O(n).

Since Java 8, when a single bucket reaches 8 entries and the table has at least 64 buckets, that bucket is converted into a red-black tree, making worst-case lookups O(log n). If the table is smaller than 64, HashMap resizes instead. When a tree shrinks to 6 entries, it turns back into a list.

Follow-up trap: trees work best when keys implement Comparable. Without it, HashMap falls back to tie-breaking rules and loses some of the benefit.

5. Why must equals() and hashCode() be overridden together?

The contract: if a.equals(b) is true, then a.hashCode() == b.hashCode() must be true.

Override equals() but not hashCode(), and two "equal" keys can land in different buckets. You put a value in, then get(sameLookingKey) returns null.

The modern answer: use a record. Records generate equals() and hashCode() from their components automatically.

record EmployeeKey(String dept, int id) {}
Enter fullscreen mode Exit fullscreen mode

6. Why are mutable keys dangerous?

If you change a field used in hashCode() after inserting the key, the entry is still sitting in the old bucket, but lookups now go to a different one. The value is effectively lost: get() returns null, and the stale entry stays in the map.

Rule of thumb: keys should be immutable. String, Integer and records are ideal.

7. Is HashMap thread-safe? What should you use instead?

No. Concurrent writes can lose updates or corrupt the structure. In Java 7, concurrent resizing could even create a loop in a bucket, causing an infinite loop on get(), because entries were inserted at the head of the list. Java 8 switched to tail insertion, but HashMap is still not safe for concurrent use.

Use ConcurrentHashMap. It locks at the bucket level and uses CAS for empty buckets, so reads are mostly lock-free.

Follow-up trap: ConcurrentHashMap does not allow null keys or null values, while HashMap allows one null key (stored in bucket 0) and many null values. The reason: in a concurrent map, get() returning null would be ambiguous. Is the key missing, or is the value null?

Bonus: what about ordering?

HashMap gives no ordering guarantee. Use LinkedHashMap for insertion order (or access order, for LRU caches) and TreeMap for sorted keys. In Java 21, LinkedHashMap implements the new SequencedMap interface, so you get firstEntry(), lastEntry() and reversed() directly.

Quick revision checklist

  • hashCode() picks the bucket, equals() picks the entry
  • Capacity 16, load factor 0.75, resize past 12 entries
  • Treeify at 8 entries per bucket when the table has 64+ buckets
  • Resize splits buckets using hash & oldCapacity
  • Immutable keys, ideally records
  • ConcurrentHashMap for concurrency, and no nulls there
  • HashMap.newHashMap(n) to pre-size (Java 19+)

If you're preparing for Java backend interviews, I put together a guide with 240 questions like this one, covering Core Java, Spring Boot 3, JPA, microservices and system design. There's a free 8-page sample on my profile.

Which HashMap follow-up caught you off guard in an interview? Tell me in the comments.

Top comments (0)