JAVA • COLLECTIONS AND STRINGS

HashSet — Use HashSet for membership testing and removing duplicates

Learn how Java's HashSet delivers lightning-fast lookups and automatically eliminates duplicate values in your data.

Historical Context & Motivation

Imagine you are building an app that tracks which students have already signed in to a school event. As hundreds of students swipe their IDs, you need to instantly check whether someone has already checked in and prevent duplicate entries. Using a plain list or array, you would have to scan through every single entry each time — a process that gets painfully slow as the list grows. This exact problem motivated computer scientists to develop a data structure called a hash set, which can answer "is this element already here?" almost instantly.

The idea behind hash-based data structures stretches back decades. The concept of hashing — converting data into a numeric index — was explored as early as the 1950s. Over time, these techniques were refined and eventually built into programming languages so developers could use them without reinventing the wheel. Java introduced its Collections Framework in version 1.2, giving programmers a powerful, ready-made HashSet class.

1953
Hash Tables Conceived
Hans Peter Luhn at IBM proposed the concept of hashing to quickly store and retrieve data, laying the foundation for modern hash-based structures.
1995
Java 1.0 Released
Sun Microsystems released Java with basic data structures like Vector and Hashtable, but lacked a unified collections system.
1998
Java 1.2 — Collections Framework
Java 1.2 introduced the Collections Framework, including the HashSet class, giving developers a clean, standardized way to manage sets of unique elements.
2004
Java 5 — Generics Added
Generics arrived, allowing type-safe declarations like HashSet<String>. This eliminated risky type-casting and made HashSet safer and easier to use.
2014
Java 8 — Streams & Lambdas
Java 8 integrated streams and functional-style operations, making it even easier to filter, transform, and collect data into HashSets.

So the central question is: how can we check whether an element exists in a collection — and guarantee that no duplicates sneak in — without scanning through every item one by one? The answer is HashSet, and understanding it will make you a significantly more effective Java programmer.

Core Principles & Definitions

Before diving into code, let's nail down the key ideas that make a HashSet tick. A Set in mathematics is a collection of distinct objects — no repeats allowed. Java's HashSet implements this idea using a technique called hashing, which converts each element into a numeric code that determines where the element is stored internally. This is what makes lookups so fast.

1

No Duplicates

A HashSet automatically rejects any element that is already present. If you add "Alice" twice, the set still contains only one "Alice".
2

Constant-Time Lookups

The contains() method runs in O(1) average time. Whether the set holds 10 items or 10 million, the check takes roughly the same time.
3

Unordered

A HashSet does not maintain the order in which elements were added. If you need ordering, consider LinkedHashSet or TreeSet instead.
4

Backed by a Hash Table

Internally, HashSet uses a HashMap. Each element's hashCode() determines which "bucket" it goes into, enabling near-instant retrieval.
5

Null-Friendly (Once)

A HashSet can hold at most one null element. Adding null a second time has no effect because duplicates are always rejected.
✦ KEY TAKEAWAY
Think of a HashSet like a coat-check room at a concert. When you hand over your coat, the attendant assigns it a numbered hook based on the first letter of your last name. To find your coat later, they don't search every hook — they go straight to the one matching your name. That instant lookup is exactly what hashing provides, and the "no duplicates" rule means no two identical coats end up on the rack.

Visual Explanation — How Hashing Places Elements

The following diagram shows how a HashSet stores four strings. Each string is passed through a hash function that converts it into a bucket index. Notice that "cat" and "bat" could potentially land in the same bucket — this is called a collision. The HashSet handles collisions internally so you don't have to worry about them.

Each element is run through a hash function to determine its bucket index. When "cat" and "bat" hash to the same bucket (index 5), the HashSet chains them together, still allowing O(1) average lookup.

As shown in the diagram, the hash function converts each element to a bucket index. The element "apple" lands in bucket 3, "dog" in bucket 1, and both "cat" and "bat" end up in bucket 5. When a collision occurs like this, the HashSet chains the elements together inside that bucket. When you call contains("bat"), Java jumps directly to bucket 5 and checks only the few elements chained there — not all four elements in the entire set.

How HashSet Works Under the Hood

Understanding the internal mechanism of a HashSet helps you write better, faster code. There are two critical methods that every object stored in a HashSet must properly implement: hashCode() and equals(). The hashCode() method produces an integer that tells the HashSet which bucket to look in. The equals() method then checks whether two objects in the same bucket are truly identical.

BUCKET INDEX FORMULA
bucketIndex = hashCode(element) % numberOfBuckets
Where hashCode(element) is the integer hash code returned by the element's hashCode() method, and numberOfBuckets is the current internal array size (default 16).
AVERAGE TIME COMPLEXITY
T(contains) = O(1) average case
In the average case, add(), remove(), and contains() all run in constant time. In the worst case (all elements collide into one bucket), performance degrades to O(n).

The add() Process Step by Step

  1. Compute hash: Java calls the element's hashCode() and maps it to a bucket index.
  2. Check bucket: If the bucket is empty, the element is inserted immediately.
  3. Handle collision: If the bucket already has elements, Java uses equals() to check if any existing element matches. If a match is found, the duplicate is rejected. If no match, the new element is chained in.
  4. Resize if needed: When the number of elements exceeds the load factor (default 0.75 × capacity), the internal array doubles in size and all elements are rehashed.
⚠️ Important Rule
If two objects are considered equal by the equals() method, they must return the same hashCode(). If you override equals() in a custom class, always override hashCode() too. Breaking this contract causes the HashSet to malfunction — it might store duplicates or fail to find elements that are present.

Key HashSet Methods & Usage Patterns

Java's HashSet class provides a small but powerful set of methods. The table below shows the most commonly used operations, their time complexities, and what they return. Mastering these methods covers the vast majority of what you will do with HashSets in practice.

Common HashSet methods and their complexities
MethodDescriptionReturn TypeTime
add(E e)Inserts element if not already presentbooleanO(1) avg
contains(Object o)Returns true if the element is in the setbooleanO(1) avg
remove(Object o)Removes the element if presentbooleanO(1) avg
size()Returns the number of elementsintO(1)
isEmpty()Returns true if the set has no elementsbooleanO(1)
clear()Removes all elements from the setvoidO(n)
The top flow shows how constructing a HashSet from a list automatically strips duplicates. The bottom flow traces a contains() call: compute the hash, jump to the bucket, and check for a match.

Common Usage Patterns

There are two patterns you'll use over and over. First, removing duplicates from a list: simply pass the list into the HashSet constructor with new HashSet<>(myList). Second, membership testing: build a HashSet of known values, then call contains() to check if a given element belongs to that set. Both patterns leverage the O(1) performance that makes HashSet so valuable.

Worked Example — Finding Unique Visitors

Suppose you have a log of website visitor usernames stored in a String array. Some users visited multiple times, so there are duplicates. Your task is to (1) find how many unique visitors there are and (2) check whether a specific user, "charlie", visited the site.

Finding Unique Visitors with HashSet
1
Step 1 — Declare the dataStart with an array of visitor usernames, which may include duplicates: String[] log = {"alice", "bob", "charlie", "alice", "dave", "bob", "eve", "charlie"}; This array has 8 elements, but only 5 are unique.
2
Step 2 — Create a HashSet and add elementsCreate a HashSet<String> and add each element from the array. You can do this with a loop, or more elegantly by converting the array to a list first: HashSet<String> uniqueVisitors = new HashSet<>(Arrays.asList(log)); The constructor automatically discards duplicates during insertion.
uniqueVisitors now holds: {"alice", "bob", "charlie", "dave", "eve"} (order may vary)
3
Step 3 — Count unique visitorsCall the size() method to find how many unique visitors there are: int count = uniqueVisitors.size();
count = 5
4
Step 4 — Test membershipUse contains() to check if "charlie" visited: boolean visited = uniqueVisitors.contains("charlie"); The HashSet hashes "charlie", jumps to the correct bucket, and finds a match.
visited = true
5
Step 5 — Put it all togetherThe complete program: import java.util.*; public class VisitorLog { public static void main(String[] args) { String[] log = {"alice","bob","charlie","alice","dave","bob","eve","charlie"}; HashSet<String> uniqueVisitors = new HashSet<>(Arrays.asList(log)); System.out.println("Unique visitors: " + uniqueVisitors.size()); System.out.println("Charlie visited? " + uniqueVisitors.contains("charlie")); } }
Output: Unique visitors: 5 | Charlie visited? true

HashSet vs. Other Collections

When should you reach for a HashSet instead of an ArrayList, TreeSet, or LinkedHashSet? The answer depends on what you need: speed, ordering, or sorted access. The table below compares these four commonly used Java collections so you can make informed decisions.

Comparison of common Java collections
FeatureHashSetArrayListTreeSet
Allows duplicates?NoYesNo
Maintains order?NoYes (insertion)Yes (sorted)
contains() timeO(1) averageO(n)O(log n)
add() timeO(1) averageO(1) amortizedO(log n)
Best forFast lookups, dedupIndexed accessSorted unique data
Allows null?Yes (one)Yes (many)No
✦ WHEN TO CHOOSE HASHSET
Choose a HashSet when your primary concern is asking "is this item in my collection?" as fast as possible, or when you need to strip duplicates from data. If you also need sorted output, use a TreeSet. If you need to preserve insertion order, use a LinkedHashSet. Think of it like choosing a tool: a HashSet is a power drill — it does one job (lookups) extremely fast — while an ArrayList is more like a Swiss Army knife with many uses but slower at any one task.

Connecting to Advanced Concepts

Once you're comfortable with HashSet, you're ready to explore related structures that build on the same hashing principles. The HashMap class, for example, works almost identically to HashSet but stores key-value pairs instead of standalone elements. In fact, HashSet is literally implemented using a HashMap behind the scenes — the elements you add become the keys, and a dummy constant is used as the value.

From HashSet to advanced topics
ConceptWhat You Know (HashSet)What's Next
Data storedIndividual elementsHashMap: key-value pairs
OrderingUnorderedLinkedHashSet: insertion order; TreeSet: sorted order
Thread safetyNot thread-safeConcurrentHashMap.newKeySet() for concurrent access
ImmutabilityMutable (can add/remove)Set.of() or Collections.unmodifiableSet() for read-only sets
Set operationsManual retainAll(), addAll()Streams: set1.stream().filter(set2::contains)...

You can also perform classic set theory operations with HashSet. Calling set1.retainAll(set2) gives you the intersection (elements in both sets), while set1.addAll(set2) produces the union (all elements from both). These operations connect directly to set theory from math class, making HashSet a bridge between abstract mathematics and practical programming.

Practice Problems

PROBLEM 1 — CONCEPTUAL
Explain why a HashSet does not allow duplicate elements. What happens internally when you try to add an element that already exists in the set?
PROBLEM 2 — BASIC CALCULATION
Given the following code, what is the output? HashSet<Integer> nums = new HashSet<>(); nums.add(5); nums.add(10); nums.add(5); nums.add(15); nums.add(10); System.out.println(nums.size()); System.out.println(nums.contains(7));
PROBLEM 3 — INTERMEDIATE
Write a method called removeDuplicates that takes an ArrayList<String> as a parameter and returns a new ArrayList<String> with all duplicate values removed, preserving the order of first appearance. Hint: use a HashSet as a helper.
PROBLEM 4 — APPLIED
A school has two clubs: Science Club and Math Club. Their member lists are stored in two arrays: String[] science = {"Ana", "Ben", "Carlos", "Diana"}; String[] math = {"Ben", "Diana", "Eve", "Frank"}; Using HashSets, write code to find (a) students in both clubs and (b) all students across both clubs combined (no duplicates).
PROBLEM 5 — CRITICAL THINKING
Consider a custom Student class with fields String name and int id. You create two Student objects with identical name and id values and add both to a HashSet<Student>. Will the set contain one student or two? Explain why, and describe what you would need to change to make it work correctly.

Summary

Java's HashSet is a collection that stores unique elements only and provides O(1) average-time performance for add(), remove(), and contains() operations. Internally, it uses a hash table that maps each element's hashCode() to a bucket index, enabling lightning-fast lookups. When two elements hash to the same bucket (a collision), they are chained together and compared using equals().

The two most important use cases are membership testing (using contains() to check if an element exists) and removing duplicates (by passing a collection into the HashSet constructor). Remember that HashSet is unordered, and when using custom objects, you must override both hashCode() and equals() to ensure correct behavior. Compared to ArrayList's O(n) lookups, HashSet is the clear choice when fast membership checks are your priority.

Varsity Tutors • Java • HashSet — Use HashSet for membership testing and removing duplicates