Java HashSet

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: 3

The add method returns a boolean that tells you whether the item was new:

  • true when the item was added because it was not there before.
  • false when 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

true
true
false

⚑ 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

true
false

πŸ” 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

10
30

The everyday methods you will reach for most:

  • add(item) puts an item in and returns false if it was already present.
  • remove(item) deletes that value and returns true if it was actually there.
  • contains(item) tells you true or false for membership, very fast.
  • size() gives the number of items in the set.
  • isEmpty() returns true when 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.
  • retainAll keeps shared names, addAll merges both, removeAll strips 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 Object compare 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: 2

The 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: 1

Override 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 added
HashSet<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 kept
java.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 HashSet
class 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. contains beats scanning a List by a wide margin.
  • Never depend on order. Pick LinkedHashSet for insertion order or TreeSet for sorted order.
  • Always override equals and hashCode together for any custom class you store.
  • Remove duplicates from a list by copying it into a HashSet in one line.
  • Use addAll, retainAll, and removeAll for 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() and equals(), so override both for your own classes.
  • βœ… add returns false for a repeat, and null is allowed once.
  • βœ… addAll, retainAll, removeAll give 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. 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. 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. 3

    Why is a HashSet good for membership checks?

    Why: HashSet's contains is very fast, unlike scanning a List item by item.

  4. 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.

Java LinkedHashSet

Share & Connect