In a real-world project, I encountered a problem with HashSet while building a simple deduplication layer to handle incoming records. At first glance, it seemed straightforward: simply store some IDs, check membership, delete expired records, and move on. However, once the code entered the actual traffic loop, the same few errors repeatedly occurred—null references, modifications to the collection during iteration, and custom objects that appeared equal to me but were not equal in Java.
It was then that I stopped treating HashSet as a “basic collection” and began investigating its actual behavior under various extreme conditions.
Understanding Java HashSet
HashSet stores elements in a hash table, meaning duplicate values are not allowed. Unlike List, it does not preserve insertion order. In practice, it’s very useful to me when I need to quickly check membership relationships and don’t care about the order.
The operations I used most often were:
add()– Adds an element if it does not already existremove()– Removes a specified elementcontains()– Checks if an element exists in the setiterator()– Iterates over elements
The catch is that the class is simple on the surface, but the failure modes are not always obvious. In my case, the bugs did not show up as neat compile-time errors. They surfaced later as exceptions or, worse, as behavior that looked “kind of correct” until I traced it carefully.
The most common exceptions I had to deal with were:
NullPointerExceptionConcurrentModificationException
Common Exceptions in HashSet and How to Handle Them
1. NullPointerException
The first problem I encountered was incredibly simple, almost embarrassing. I declared a collection but didn’t initialize it before using it. In my local tests, I only encountered this issue once because the code path was hidden behind a conditional statement, so I initially thought the problem was elsewhere.
A NullPointerException occurs when trying to perform an operation on a null HashSet reference or when inserting a null element if the underlying logic doesn’t support it.
import java.util.HashSet;
public class HashSetNullPointerExample {
public static void main(String[] args) {
HashSet<String> set = null;
try {
// Attempting to add an element to a null HashSet
set.add("Apple");
} catch (NullPointerException e) {
System.out.println("Caught NullPointerException: Initialize your HashSet first!");
}
// Proper initialization
set = new HashSet<>();
set.add("Apple");
set.add("Banana");
System.out.println("HashSet after initialization: " + set);
}
}
What I learned here was painfully straightforward: a HashSet must be instantiated before adding elements. The exception itself is not subtle, but in a larger codebase the source can be hidden behind a few layers of method calls. Once I saw the stack trace clearly, the fix was just one line — but it saved me from chasing a fake bug for half an hour.
Insights:
A HashSet must be instantiated before adding elements.NullPointerException usually means an uninitialized or null reference, not a problem with HashSet itself.
2. ConcurrentModificationException
This one was more annoying because the code looked clean. I was looping through the set with an enhanced for-loop and removing an element in the middle of the loop. In a small test set, it sometimes looked like it worked. Under a slightly larger dataset, it broke immediately.
This exception occurs when a HashSet is modified while iterating over it using an enhanced for-loop.
import java.util.HashSet;
import java.util.Iterator;
import java.util.ConcurrentModificationException;
public class HashSetConcurrentModification {
public static void main(String[] args) {
HashSet<String> fruits = new HashSet<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
try {
for (String fruit : fruits) {
if (fruit.equals("Banana")) {
fruits.remove(fruit); // Unsafe modification
}
}
} catch (ConcurrentModificationException e) {
System.out.println("Caught ConcurrentModificationException: Cannot modify HashSet during iteration.");
}
// Safe removal using Iterator
Iterator<String> iterator = fruits.iterator();
while (iterator.hasNext()) {
if (iterator.next().equals("Banana")) {
iterator.remove();
}
}
System.out.println("HashSet after safe removal: " + fruits);
}
}
At this point, I had to slow down and stop assuming the loop “is just a loop.” Directly modifying the HashSet during iteration would trigger a ConcurrentModificationException, which the JVM would quickly point out. Using Iterator solved this problem and stabilized the behavior.
Explanation:
Modifying a HashSet directly during iteration triggers ConcurrentModificationException.
Using an Iterator allows safe removal without breaking the iteration.
3. IllegalArgumentException (Rare Scenario)
This one is a little different. In my project, this did not show up as a classic HashSet exception in the normal sense. What happened was that our custom object had sloppy hashCode() and equals() behavior, so the set started acting inconsistently. At first, it looked like a collection problem. After tracing the object comparisons, I realized the real issue was the class itself.
Although uncommon, IllegalArgumentException may occur if custom objects in a HashSet have inconsistent hashCode() and equals() methods, violating the contract of a Set.
import java.util.HashSet;
class CustomItem {
String name;
CustomItem(String name) {
this.name = name;
}
// hashCode and equals not overridden properly
}
public class HashSetIllegalArgument {
public static void main(String[] args) {
HashSet<CustomItem> items = new HashSet<>();
try {
items.add(new CustomItem("Item1"));
items.add(new CustomItem("Item1")); // May cause logical issues
} catch (IllegalArgumentException e) {
System.out.println("Caught IllegalArgumentException: Check equals() and hashCode() methods!");
}
System.out.println("HashSet size: " + items.size());
}
}
What I took away from this was simple: HashSet depends heavily on hashCode() and equals() for uniqueness. If those methods are inconsistent, the set may not behave the way you expect. In our case, the symptom was duplicate-looking entries and failed lookups, which took longer to debug than a normal exception because nothing was crashing right away.
Insights:
HashSet relies on hashCode() and equals() for uniqueness.
Overriding these methods correctly in custom objects prevents unexpected behavior.
After resolving these issues in the project, I eventually summarized some rules that I now follow without hesitation:
Always initialize a HashSet before use.
Avoid modifying a HashSet during iteration; use an Iterator to safely remove elements.
Handle NullPointerException gracefully using try-catch blocks.
Ensure that the implementations of hashCode() and equals() in custom objects are correct.
Use clear exception messages in catch blocks for easier debugging.
The main lesson for me is that HashSet itself isn’t difficult, but it can become very tricky if the surrounding code isn’t rigorous. Most of the problems I encountered weren’t actually with the set itself, but rather stemmed from some assumptions I made while rushing through my code. Once I started examining initialization, iteration behavior, and object equality more carefully, the problems became much easier to pinpoint.