
HashSet in Java
The `HashSet` is a collection that uses a hash table for storage. Elements are stored by hashing and as a result, it only supports unique elements.
July 24, 2024 · 8 min read
The HashSet is a collection that uses a hash table for storage. Elements are stored by hashing and as a result, it only supports unique elements.
Key Properties of HashSet
Unlike most other collections, a HashSet does not keep track of order.
A HashSet is preferred when your data is unique and the order of elements is not required. HashSet performs faster for basic operations like add, remove, and contains, which typically run in constant time, O(1) — but that O(1) depends entirely on your elements having a good hashCode() implementation, which is worth understanding.
How it actually works
HashSet isn't really its own data structure — it's a thin wrapper around a HashMap<E, Object>. Every element you add becomes a key in that backing map, paired with a shared dummy value (a private static final Object PRESENT). add() is really map.put(element, PRESENT), contains() is map.containsKey(element), and so on. Everything about HashSet's performance and behaviour — bucket layout, resizing, treeification of long collision chains — is really HashMap's behaviour. The HashMap article covers that internal mechanism in more depth.
The O(1) average case assumes hashCode() spreads your elements evenly across buckets. A poor hashCode() (or none at all, falling back to Object's identity-based default) that clusters everything into a handful of buckets degrades lookups toward O(n) in the worst case, since Java 8+, an over-full bucket becomes a red-black tree instead of a linked list, capping the true worst case at O(log n) — but you still lose the O(1) you were counting on.
The mutable element trap
This is the single most common HashSet bug: mutate an element's hashCode()-relevant fields after adding it, and the set effectively loses track of it.
class Point {
int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override
public boolean equals(Object o) {
if (!(o instanceof Point p)) return false;
return x == p.x && y == p.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
}
Set<Point> points = new HashSet<>();
Point p = new Point(1, 2);
points.add(p);
p.x = 99; // mutated after insertion
System.out.println(points.contains(p)); // false — still stored in the (1,2) bucket, but now hashes to a different one
System.out.println(points.contains(new Point(99, 2))); // also false — that bucket doesn't have this objectp is still physically inside the set — iterating points will print it — but contains() and remove() both recompute the hash from p's current state to find the bucket to look in, and that bucket is no longer where p actually lives. The fix is simple: never use mutable fields in equals()/hashCode() for objects you plan to put in a HashSet or use as a HashMap key. If you need to change something about an object that's already in a set, remove it first, mutate it, then re-add it.
Creating and initializing HashSet
// Creating a HashSet using the default constructor
HashSet<String> hashSet = new HashSet<>();
hashSet.add("A");
hashSet.add("B");
hashSet.add("C");
// Creating a HashSet with an initial capacity
HashSet<String> hashSetWithCapacity = new HashSet<>(50);
// Creating a HashSet with an initial capacity and load factor
HashSet<String> hashSetWithLoadFactor = new HashSet<>(50, 0.75f);
// Creating a HashSet from another collection
List<String> list = Arrays.asList("A", "B", "C", "D");
HashSet<String> hashSetFromList = new HashSet<>(list);
// Using the factory method
Set<String> set = Set.of("A", "B", "C");
HashSet<String> hashSetFromSet = new HashSet<>(set);- The load factor defines how full the hash table is allowed to get before its capacity is automatically increased.
Iterate HashSet Elements
Since a HashSet doesn't support indexed access, you'll need an iterator (or one of its Java 8 equivalents) to walk through its elements. Its iterator is fail-fast — it throws ConcurrentModificationException if the set is structurally modified any way other than through the iterator itself while you're iterating. Also note that iteration order is not the insertion order — it's whatever order the backing hash table happens to lay elements out in, which can change entirely after a resize.
HashSet<String> hashSet = new HashSet<>(Arrays.asList("A", "B", "C", "D"));
// Using an enhanced for-each loop
for (String element : hashSet) {
System.out.println(element);
}
// Using an Iterator
Iterator<String> iterator = hashSet.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
System.out.println(element);
}
// Using a Java 8 Spliterator
Spliterator<String> spliterator = hashSet.spliterator();
spliterator.forEachRemaining(System.out::println);
// Using the forEach() method (Java 8+)
hashSet.forEach(element -> System.out.println(element));
// Using a Stream (Java 8+)
hashSet.stream().forEach(System.out::println);- A
Spliteratorcan be used for traversing and partitioning elements of a source. It is particularly useful for parallel processing.
Removing Items in a HashSet
There are several methods available to remove an item from HashSet.
// Remove a single element using remove()
HashSet<String> hashSet = new HashSet<>(Arrays.asList("A", "B", "C", "D"));
hashSet.remove("B");
// Remove elements conditionally using a predicate
HashSet<String> hashSet1 = new HashSet<>(Arrays.asList("A", "B", "C", "D"));
hashSet1.removeIf(s -> s.startsWith("A"));
// Remove all elements
HashSet<String> hashSet2 = new HashSet<>(Arrays.asList("A", "B", "C", "D"));
hashSet2.clear();
// Remove an element using an Iterator
HashSet<String> hashSet3 = new HashSet<>(Arrays.asList("A", "B", "C", "D"));
Iterator<String> iterator = hashSet3.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
if ("B".equals(element)) {
iterator.remove();
}
}
// Retain specific elements and remove the rest
HashSet<String> hashSet4 = new HashSet<>(Arrays.asList("A", "B", "C", "D"));
hashSet4.retainAll(Arrays.asList("A", "C"));
// Remove elements found in another collection
HashSet<String> hashSet5 = new HashSet<>(Arrays.asList("A", "B", "C", "D"));
hashSet5.removeAll(Arrays.asList("A", "C")); // Removes "A" and "C" from the HashSetHashSet vs the Alternatives
- vs
LinkedHashSet: same O(1) average performance, butLinkedHashSetalso maintains insertion order at the cost of two extra reference fields per entry. If you don't specifically need predictable iteration order, plainHashSetuses less memory for the same data. - vs
TreeSet:HashSetis O(1) average vsTreeSet's O(log n), butTreeSetkeeps elements sorted and supports range queries (headSet,tailSet,ceiling, etc.). Choose based on whether you need order at all — if you don't,HashSetis strictly faster. - vs
Collections.newSetFromMap(new ConcurrentHashMap<>()):HashSetisn't thread-safe. For concurrent access, wrap it withCollections.synchronizedSet(), or use a set backed byConcurrentHashMapfor lock-striped concurrent access instead of a single lock around the whole set.
