The HashSet Class
Objective
Understand HashSet, the default Set implementation: elements are stored in a hash table, giving average constant-time add/remove/contains at the cost of any guaranteed iteration order.
Use Cases
- Fast membership testing (
contains) when order genuinely doesn't matter. - Deduplicating a collection of values quickly.
- Pre-sizing the table via the capacity/fill-ratio constructor when the eventual element count is roughly known, to avoid rehashing during a large batch of inserts.
Deep Dive
HashSet extends AbstractSet
javaclass HashSet<E>HashSet implements Set<E> and defines no additional methods beyond what AbstractSet/Set/Collection already provide — its contribution is entirely in how elements are stored, not in new API surface. Four constructors:
javaHashSet<String> a = new HashSet<>(); // default capacity 16, load factor 0.75
HashSet<String> b = new HashSet<>(List.of("x", "y")); // initialized from a collection
HashSet<String> c = new HashSet<>(64); // initial capacity 64
HashSet<String> d = new HashSet<>(64, 0.5f); // capacity 64, load factor 0.5The load factor (also called fill ratio) controls how full the table can get, as a fraction of capacity, before it's resized upward — 0.75 by default, meaning the table roughly doubles once it's three-quarters full.
Lookup relies on hashCode() and equals()
An element's hash code determines which bucket it lands in; equals() then distinguishes elements that share a bucket. Both must be correct and consistent with each other for add/contains/remove to behave correctly — this is the same Object contract every hash-based structure in the JDK depends on.
Watch it happen: add() spreading elements across buckets
Each add(element) computes element.hashCode(), spreads its bits, and masks it against capacity - 1 to pick a bucket — the same mechanism HashMap uses, since HashSet is a thin wrapper around one:
Iteration order is unspecified
javaHashSet<String> hs = new HashSet<>();
hs.add("Beta"); hs.add("Alpha"); hs.add("Eta");
hs.add("Gamma"); hs.add("Epsilon"); hs.add("Omega");
System.out.println(hs); // [Gamma, Eta, Alpha, Epsilon, Omega, Beta] — order is table-layout dependent, not insertion orderThe exact order depends on each element's hash code and the table's current capacity, not the order elements were added.
Trade-offs
No iteration-order guarantee, and it can change between runs or after resizing — if a stable order matters, use
LinkedHashSet(insertion order); if a sorted order matters, useTreeSet.Mutating a field involved in an element's
hashCode()after it's already in the set breaks lookups silently — the element sits in the bucket its old hash code pointed to, socontains()/remove()with an equal-looking object can returnfalse/no-op instead of finding it:javaclass Point { int x; /* hashCode() based on x */ } Point p = new Point(1); HashSet<Point> set = new HashSet<>(); set.add(p); p.x = 2; // mutated after insertion set.contains(p); // may return false — p is now in the wrong bucketAverage O(1) operations assume a reasonably distributed
hashCode()— a poor hash function that collides heavily degradesadd/contains/removetoward O(n), since every colliding element has to be checked withequals().HashSetis not synchronized — same caveat asArrayList/LinkedListfor concurrent or in-loop structural modification.