Java HashSet
Table of Contents + β
In the last lesson you learned about the Java Set interface. For things like tags, unique visitors, or the distinct words in a document, duplicates make no sense. Java has a collection built exactly for unique items: the HashSet. It throws away duplicates automatically and checks membership very fast.
π€ Why a Set instead of a List?
With a List you would check βis this already here?β before every add. A HashSet does that for you:
- It blocks duplicates on its own. Adding an item that is already there is ignored.
- It keeps your code short. No manual βdoes this exist?β loop.
- It checks membership fast, even when the set is huge.
Think of a guest list at a private event. The person at the door lets each named guest in once. A second copy of a name gets waved away. They do not care about arrival order. A HashSet is that person at the door.
π§© What is a HashSet?
A HashSet is a collection that stores only unique items with very fast lookups. Two traits to always keep in mind:
- It ignores duplicates. Adding an existing item changes nothing.
- It has no guaranteed order. Print or loop, and the order can look random.
That second point surprises people. A HashSet trades ordering for speed. If you need order, Java has other sets, which we cover later.
How it works under the hood
A HashSet is backed by a hash table. The basic idea explains everything else:
- When you add an item, Java calls its
hashCode()to get a number. - That number picks a βbucketβ, so Java jumps straight to the right spot later.
- To check membership, Java goes to that one bucket and compares with
equals().
Because Java jumps to a bucket instead of scanning, add, remove, and contains all run in O(1) on average. O(1) means the work stays about the same no matter how big the set grows. A List scans item by item, which is O(n). That gap is the whole reason a HashSet exists.
π‘ Removing duplicates automatically
Letβs see the no-duplicates behavior. The code below adds some names, including a repeat, and the set keeps only the unique ones.
import java.util.HashSet;
public class Main { public static void main(String[] args) { HashSet<String> names = new HashSet<>(); names.add("Alex"); names.add("Riya"); names.add("Alex"); // β duplicate, ignored names.add("Arjun");
System.out.println(names); System.out.println("Size: " + names.size()); }}We added βAlexβ twice, but the set keeps one:
- Size is 3, not 4. The second βAlexβ was ignored.
- The printed order may not match the order we added them.
Output
[Riya, Arjun, Alex]Size: 3The add method returns a boolean that tells you whether the item was new:
truewhen the item was added because it was not there before.falsewhen the item already existed, so nothing changed.
The next example uses that to detect a repeat without an extra lookup.
import java.util.HashSet;
public class Main { public static void main(String[] args) { HashSet<String> emails = new HashSet<>();
System.out.println(emails.add("alex@mail.com")); // β
true, new System.out.println(emails.add("riya@mail.com")); // β
true, new System.out.println(emails.add("alex@mail.com")); // β false, repeat }}The third add returns false. So you instantly know that email was already signed up, with no separate contains call. Handy for βfirst time we have seen this?β checks.
Output
truetruefalseβ‘ Fast membership checks
The contains method checks if something is in the set. It is very fast, even for huge sets, thanks to the hash table.
import java.util.HashSet;
public class Main { public static void main(String[] args) { HashSet<String> allowed = new HashSet<>(); allowed.add("admin"); allowed.add("editor"); allowed.add("viewer");
System.out.println(allowed.contains("editor")); // β
true System.out.println(allowed.contains("guest")); // β false }}contains instantly tells us whether a role is allowed. A List would scan item by item, which gets slow as it grows. A HashSet answers almost instantly at any size. That is why sets are perfect for βis this allowed?β or βhave we seen this before?β checks.
Output
truefalseπ Looping, removing, and the core methods
You loop over a HashSet with a for-each. Just remember the order is not guaranteed. You remove items with remove, which takes the value, not an index.
import java.util.HashSet;
public class Main { public static void main(String[] args) { HashSet<Integer> numbers = new HashSet<>(); numbers.add(10); numbers.add(20); numbers.add(30);
numbers.remove(20); // β
remove the value 20
for (int n : numbers) { System.out.println(n); } }}We remove 20 by value, not by index. Sets have no positions, so there is no get(index) either.
Output
1030The everyday methods you will reach for most:
add(item)puts an item in and returnsfalseif it was already present.remove(item)deletes that value and returnstrueif it was actually there.contains(item)tells youtrueorfalsefor membership, very fast.size()gives the number of items in the set.isEmpty()returnstruewhen the set has no items.clear()empties the set in one call.
A HashSet allows null, but only once, just like any other value.
π Set operations: union, intersection, difference
Sets shine when you compare two of them. Java does not give you fancy operator symbols, but three plain methods do all the classic set math when you pass one set into another:
addAll(other)keeps everything from both sets. This is the union.retainAll(other)keeps only items that are in both sets. This is the intersection.removeAll(other)drops every item that is also in the other set. This is the difference.
The example below finds who took both courses, everyone across both, and who took only the first course.
import java.util.HashSet;
public class Main { public static void main(String[] args) { HashSet<String> javaStudents = new HashSet<>(); javaStudents.add("Alex"); javaStudents.add("Riya"); javaStudents.add("Arjun");
HashSet<String> pythonStudents = new HashSet<>(); pythonStudents.add("Riya"); pythonStudents.add("Arjun"); pythonStudents.add("Sara");
// Intersection: students in BOTH courses HashSet<String> both = new HashSet<>(javaStudents); both.retainAll(pythonStudents); System.out.println("Both courses: " + both);
// Union: every distinct student HashSet<String> everyone = new HashSet<>(javaStudents); everyone.addAll(pythonStudents); System.out.println("All students: " + everyone);
// Difference: only-Java students HashSet<String> onlyJava = new HashSet<>(javaStudents); onlyJava.removeAll(pythonStudents); System.out.println("Only Java: " + onlyJava); }}Notice we copy the first set with new HashSet<>(javaStudents) before each operation:
- These methods change the set they are called on.
- So we work on a fresh copy to leave the originals untouched.
retainAllkeeps shared names,addAllmerges both,removeAllstrips out anyone who also did Python.
Output
Both courses: [Arjun, Riya]All students: [Arjun, Sara, Alex, Riya]Only Java: [Alex]Remember the order in that output is still not meaningful. Only the membership matters.
π§ How uniqueness really works: hashCode and equals
Built-in types like String and Integer already know how to compare themselves. But put your own class into a HashSet and you must understand the rule, or duplicates sneak in.
A HashSet decides βare these two the same?β in two steps:
- First it compares
hashCode(). Different codes mean different items, and it stops there. - If the codes match, it calls
equals()to confirm they are truly equal. - The default versions from
Objectcompare by memory address, so two separate objects always look different.
The next example shows the trap. Two User objects with the same name, nothing overridden.
import java.util.HashSet;
public class Main { static class User { String name; User(String name) { this.name = name; } }
public static void main(String[] args) { HashSet<User> users = new HashSet<>(); users.add(new User("Alex")); users.add(new User("Alex")); // β looks like a duplicate, but is NOT removed
System.out.println("Size: " + users.size()); }}Both users are named βAlexβ, yet the size is 2. We never told Java how to compare User objects, so it compared memory addresses. The two new objects sit at different addresses, so the set thinks they are different people.
Output
Size: 2The fix is to override both hashCode() and equals() to look at the field that defines identity, here the name.
import java.util.HashSet;import java.util.Objects;
public class Main { static class User { String name; User(String name) { this.name = name; }
@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; User user = (User) o; return Objects.equals(name, user.name); // β
compare by name }
@Override public int hashCode() { return Objects.hashCode(name); // β
same name, same code } }
public static void main(String[] args) { HashSet<User> users = new HashSet<>(); users.add(new User("Alex")); users.add(new User("Alex")); // β
now treated as a duplicate
System.out.println("Size: " + users.size()); }}Now the second βAlexβ is a duplicate and dropped, so the size is 1. The Objects.equals and Objects.hashCode helpers keep this short and even handle null for you.
Output
Size: 1Override both, not just one
If you override equals but forget hashCode (or the reverse), the set breaks in confusing ways. The two methods are a pair. Always change both together, and make sure they look at the same fields. Most editors can generate both for you in one step.
π― A real use: removing duplicates from a list
A classic use of a HashSet is cleaning duplicates out of data. Add everything to a set and the repeats drop on their own. Here we also count distinct visitors.
import java.util.HashSet;
public class Main { public static void main(String[] args) { String[] visits = {"Alex", "Riya", "Alex", "Arjun", "Riya", "Alex"}; HashSet<String> uniqueVisitors = new HashSet<>();
for (String name : visits) { uniqueVisitors.add(name); // β
duplicates ignored automatically }
System.out.println("Distinct visitors: " + uniqueVisitors); System.out.println("Unique count: " + uniqueVisitors.size()); }}We add every visit, the duplicates drop, and the set keeps only distinct names. Six visits, but three people. The same trick cleans duplicate emails, tags, or any repeated values in one pass.
Output
Distinct visitors: [Riya, Arjun, Alex]Unique count: 3β οΈ Common Mistakes
A few HashSet slip-ups to watch for.
Expecting items to come back in order. A HashSet has no guaranteed order. Code that assumes insertion order will eventually break.
// β Wrong: assuming the first item printed is the first one addedHashSet<String> tags = new HashSet<>();tags.add("first");tags.add("second");// The order printed here is NOT promised to be "first" then "second"
// β
Right: use LinkedHashSet when you need insertion order keptjava.util.LinkedHashSet<String> ordered = new java.util.LinkedHashSet<>();ordered.add("first");ordered.add("second");Forgetting equals and hashCode on your own classes. Without them, identical-looking objects are treated as different and duplicates slip in.
// β Wrong: a class with no equals/hashCode used in a HashSetclass Point { int x, y; }// Two points with the same x and y will NOT be seen as duplicates
// β
Right: override both, comparing the same fields, so duplicates are caught// (use Objects.equals(...) and Objects.hash(x, y) inside the overrides)Trying to use an index. Sets have no get(index). There is no fixed position, so loop with for-each instead.
β Best Practices
Habits for using HashSet well.
- Reach for it whenever items must be unique, like tags, IDs, or βseen beforeβ checks.
- Use it for fast membership tests.
containsbeats scanning a List by a wide margin. - Never depend on order. Pick
LinkedHashSetfor insertion order orTreeSetfor sorted order. - Always override
equalsandhashCodetogether for any custom class you store. - Remove duplicates from a list by copying it into a
HashSetin one line. - Use
addAll,retainAll, andremoveAllfor union, intersection, and difference, and copy the set first if you want to keep the original.
π§© What Youβve Learned
Nicely done. Letβs recap HashSet.
- β A HashSet stores only unique items and ignores duplicates automatically.
- β It is backed by a hash table, so add, remove, and contains run in O(1) on average.
- β It has no guaranteed order; do not rely on how items come out.
- β
Uniqueness comes from
hashCode()andequals(), so override both for your own classes. - β
addreturnsfalsefor a repeat, andnullis allowed once. - β
addAll,retainAll,removeAllgive you union, intersection, and difference. - β
A common use is removing duplicates or counting distinct items with
size().
Check Your Knowledge
Test what you learned. Pick an answer for each question, then click Check.
- 1
What does a HashSet do when you add a duplicate item?
Why: A HashSet stores only unique items, so duplicates are silently ignored.
- 2
What is true about the order of items in a HashSet?
Why: A HashSet does not guarantee any order; use LinkedHashSet or TreeSet if order matters.
- 3
Why is a HashSet good for membership checks?
Why: HashSet's contains is very fast, unlike scanning a List item by item.
- 4
For your own class, what makes the HashSet treat two objects as the same item?
Why: A HashSet uses hashCode() and equals() to decide uniqueness, so you must override both for custom classes.
π Whatβs Next?
A HashSet is fast but unordered. What if you want unique items kept in the order you added them? Java has a set for that too: the LinkedHashSet. Letβs learn it next.