HashSet Internals
Fast recall
Quick revision
Core answer
PRESENT). So it inherits HashMap's O(1) average performance, allows one null element, and requires correct `equals()` and `hashCode()`.Understanding that Set is implemented on top of Map and the implications for performance and contract requirements
Believing HashSet has its own independent hashing mechanism instead of delegating to HashMap
Mention that this is why HashSet allows one null element (HashMap allows one null key) and that changing an object after adding it breaks the set if hashCode changes
Interview-ready guide
Interview answer
HashSet is actually built on top of HashMap.
When you add an element to a HashSet, it becomes a key in an internal HashMap, and the value is just a constant dummy object. This means HashSet gets all the benefits of HashMap's hashing, so you get constant time performance for adding, removing, and checking if something exists. It also means HashSet can hold one null element, because HashMap allows one null key.
The critical thing to remember is that your objects need proper equals and hashCode methods. If you don't override both correctly, HashSet won't work right, and you might add an element but then contains says it's not there.
A common mistake freshers make is changing an object after adding it to a HashSet. Say you add an order object, then change its id. The hashCode changes, so the object is now in the wrong bucket internally.
You can't find it or remove it anymore. The set is basically broken at that point. So the key takeaway is that HashSet delegates everything to HashMap, which is why it's fast, but it also means you have to follow the equals and hashCode contract and treat elements as immutable once they're in the set.
HashSet Internals
Direct Answer
HashSet is backed by a HashMap internally: each element you add becomes a key, paired with a constant dummy value (PRESENT). So it inherits HashMap's O(1) average performance, allows one null element, and requires correct equals() and hashCode().
Code Example
This snippet shows the behaviour that falls out of the HashMap backing: duplicate rejection, one null, and the equals/hashCode contract for custom objects.
import java.util.*;
public class OrderIdSet {
public static void main(String[] args) {
var orderIds = new HashSet<Long>();
// Behind the scenes: map.put(101L, PRESENT)
System.out.println(orderIds.add(101L)); // true (new key)
System.out.println(orderIds.add(101L)); // false (key already there)
orderIds.add(null); // allowed: one null key
System.out.println(orderIds.contains(null)); // true
// Custom object needs correct hashCode/equals
record Order(Long id, String item) {} // record auto-generates both
var orders = new HashSet<Order>();
orders.add(new Order(101L, "Laptop"));
System.out.println(orders.contains(new Order(101L, "Laptop"))); // true
}
}
The record gets correct equals/hashCode for free, so a freshly built equal Order lands in the same bucket and contains() finds it.
Common Mistakes & Pitfalls
- Mutating an element after adding it: change a field that feeds
hashCode, and the element's hashCode now points at a different bucket. It still sits in the old one, so the set can no longer find or remove it — the entry is effectively lost. - Overriding only
equals: withhashCodeleft alone, two equal objects can hash to different buckets, socontains()returns false even though the element is there. Override both together. - Assuming an iteration order:
HashSetmakes no order guarantee. Code that expects insertion or sorted order breaks when the internalHashMapresizes — useLinkedHashSetorTreeSetinstead.
Check your understanding
Answer these without looking back at the article.
- What happens if you put a mutable object in a HashSet and then change it?
- How does TreeSet know the order of elements?
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.