Back to Blog
Java

Java HashSet Duplicates: Behavior and Detection

java hashset duplicates: Explains how Java HashSet silently ignores duplicates, how equals() and hashCode() determine equality, and how to detect duplicates when addin...

HashSetJava Collectionsequals and hashCodeDuplicate DetectionSet Implementation
Illustration of a HashSet container accepting unique elements while rejecting a duplicate element, showing how duplicate detection works.

java hashset duplicates requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

The way Java HashSet handles duplicates is simple: when you call add() with an element that already exists in the set, the method returns false and the set is left unchanged. The duplicate is silently ignored rather than stored a second time. This is the defining behavior of a Set in the Java Collections Framework: a set cannot contain duplicate elements.

What Happens When You Add a Duplicate to a HashSet

When you call add() on a java.util.HashSet with an element that is already present, the method returns false and the set is not modified. No exception is thrown, and the existing element is not overwritten.

import java.util.HashSet; import java.util.Set; Set<String> names = new HashSet<>(); names.add("ada"); boolean firstAdd = names.add("ada"); System.out.println(firstAdd); // false System.out.println(names.size()); // 1

The second add("ada") call reports that the element was already present. The set still contains exactly one element.

This behavior is intentional. HashSet is backed by a HashMap internally, and each element is stored as a key in that map. Since a map cannot hold duplicate keys, the set inherits the same uniqueness guarantee.

How HashSet Decides Two Elements Are Equal

HashSet does not use reference identity to decide whether an element is a duplicate. Instead, it relies on the equals() method of the stored objects, together with hashCode().

When you call add(element), the set computes element.hashCode() to locate the correct hash bucket. If the bucket is empty, the element is inserted directly. If the bucket already contains entries, the set compares the new element with each existing entry using equals(). If any existing entry is equal to the new element, the new element is rejected.

This means two distinct object instances can be treated as duplicates if their equals() method considers them equal.

public class User { private final String email; public User(String email) { this.email = email; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof User)) return false; return email.equals(((User) o).email); } @Override public int hashCode() { return email.hashCode(); } }

With this class, two User instances with the same email address are considered equal. Adding both to a HashSet results in a set with a single element.

Set<User> users = new HashSet<>(); users.add(new User("ada@example.com")); users.add(new User("ada@example.com")); System.out.println(users.size()); // 1

If you omit equals() and hashCode(), the default Object implementations use reference identity. Two separately constructed User objects would then be considered distinct, even if every field is identical, and the set would contain both.

Why hashCode() Matters for Duplicate Detection

The hashCode() method does not determine equality by itself, but it determines how efficiently the set finds a potential duplicate. Two equal objects must have the same hash code. If they do not, the set may place them in different buckets and never compare them with equals(), allowing duplicates to slip through.

Consider a class that overrides equals() but not hashCode():

public class Product { private final String sku; public Product(String sku) { this.sku = sku; } @Override public boolean equals(Object o) { if ( this == o) return true; if (!(o instanceof Product)) return false; return sku.equals(((Product) o).sku); } }

Two Product instances with the same SKU are equal according to equals(), but they inherit Object.hashCode(), which returns different values for distinct instances. HashSet will likely place them in different buckets, never call equals(), and store both. The set now violates its own uniqueness contract.

The fix is to override hashCode() consistently with equals():

@Override public int hashCode() { return sku.hashCode(); }

The contract between the two methods is the foundation of correct HashSet behavior. Whenever you override equals(), you must override hashCode() using the same fields. This is not optional for classes that will be stored in hash-based collections.

Mutable Objects Can Break Duplicate Detection

A less obvious failure occurs when an object already stored in a HashSet is mutated after insertion. If the mutation changes the fields used by hashCode(), the object's hash code changes, but the set does not move it to a new bucket. The element remains in its original bucket, and future contains() or add() calls may fail to find it.

Set<List<Integer>> sets = new HashSet<>(); List<Integer> list = new ArrayList<>(); list.add(1); sets.add(list); list.add(2); // mutates the stored element System.out.println(sets.contains(list)); // may print false

The ArrayList hash code changes when its contents change. After the mutation, the set's internal bucket index no longer matches the object's current hash code, so contains() can return false even though the object is still in the set. Duplicate detection becomes unreliable because the set can no longer locate the element.

The practical rule is to avoid mutating objects after they are placed in a HashSet, or to remove the object before mutation and re-add it afterward. If you need mutable elements with changing identity, a Set is usually the wrong data structure.

Using add()'s Return Value to Detect Duplicates

Because add() returns false when the element is already present, you can use that return value to detect duplicates at insertion time without scanning the entire set.

Set<String> seen = new HashSet<>(); List<String> input = List.of("alpha", "beta", "alpha", "gamma"); for (String value : input) { if (!seen.add(value)) { System.out.println("Duplicate found: " + value); } }

This pattern is useful for filtering input, validating that a collection contains unique values, or counting how many duplicates appear in a stream. The add() call performs the duplicate check in constant time on average, so the loop runs in linear time relative to the input size.

If you need to know how many times each element appears, a HashMap with a counter is more appropriate than a HashSet, because the set discards the duplicate information entirely.

Performance Characteristics of HashSet Duplicate Checks

HashSet offers constant-time average complexity for add(), contains(), and remove(), assuming the hash function distributes elements evenly across buckets. Duplicate detection therefore does not require scanning the entire collection, which is the main reason to prefer a HashSet over a List for uniqueness checks.

The cost of a duplicate check is dominated by two operations: computing hashCode() and, when a bucket collision occurs, calling equals() on the colliding entries. If hashCode() is expensive, every add() call pays that cost even for unique elements. If many elements share the same hash code, the set degrades toward a linked list within a single bucket, and duplicate checks become linear in the number of colliding elements.

A well-designed hashCode() that distributes values evenly keeps the average bucket size near one and keeps duplicate detection fast. The initial capacity and load factor of the HashSet also affect performance. When the number of elements is known in advance, constructing the set with an appropriate initial capacity avoids repeated resizing during insertion.

Choosing Between HashSet, LinkedHashSet, and TreeSet

When duplicate handling is the main concern, the choice of set implementation affects ordering and comparison behavior, not the basic duplicate rule.

ImplementationOrderingDuplicate ruleBest fit
HashSetUnorderedBased on equals() and hashCode()Fast lookups, no ordering requirement
LinkedHashSetInsertion orderSame as HashSetPreserving insertion order while keeping fast lookups
TreeSetSorted by natural order or comparatorBased on compareTo() or comparatorSorted iteration and range queries

TreeSet does not use equals() and hashCode() for duplicate detection. It uses the Comparator or the natural ordering of the elements. Two elements are considered duplicates when compareTo() returns zero. This means a TreeSet can reject elements that equals() would consider distinct, or accept elements that equals() would consider equal, depending on how the comparator is written.

For most duplicate-detection tasks, HashSet is the right default. Choose LinkedHashSet when you need predictable iteration order without sacrificing hash-based lookup speed. Choose TreeSet only when sorted iteration or range operations are an actual requirement, because the logarithmic insertion cost is higher than the constant-time behavior of HashSet.

java hashset duplicates: Practical Usage and Code Examples | RYUSLOG DEV