HashSet in Java – Complete Guide with Examples
HashSet is the most widely used Set implementation in Java. It stores unique elements in a hash table, which makes adding, removing and checking an element very fast, usually constant time, no matter how many elements the set holds.
It lives in java.util and has been part of the Collections Framework since JDK 1.2. In this tutorial you will learn how to use HashSet, how hashing works inside it, and the rules your own classes must follow to be stored in it correctly.
1. What Is a HashSet?
Imagine a coat check with numbered hooks. The attendant uses a rule to turn your ticket into a hook number, so finding your coat takes one step instead of searching every hook. A HashSet does the same with a hash code: it turns each element into a bucket number and goes straight to that bucket.
- Unique elements: duplicates are ignored.
- No guaranteed order: elements come back in an order based on their hash codes.
- Fast:
add(),remove()andcontains()are O(1) on average. - One
nullallowed. - Backed by a HashMap: each element is stored as a key of an internal
HashMap. - Not synchronized and with fail-fast iterators.
2. Class Hierarchy and Declaration
public class HashSet<E> extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
LinkedHashSet extends HashSet and adds insertion order.
3. Your First HashSet Program
import java.util.HashSet;
import java.util.Set;
public class HashSetQuickStart {
public static void main(String[] args) {
Set<String> tags = new HashSet<>();
tags.add("java");
tags.add("spring");
tags.add("java"); // ignored: already present
tags.add(null); // one null is allowed
System.out.println("Tags: " + tags);
System.out.println("Size: " + tags.size());
System.out.println("contains(spring) " + tags.contains("spring"));
}
}
Tags: [spring, null, java] Size: 3 contains(spring) true
"java" was added twice but stored once, and the printed order is not the order of insertion. Never write code that depends on a HashSet's iteration order.
4. Constructors
| Constructor | What it does |
|---|---|
HashSet() | Empty set with the default capacity (16) and load factor (0.75). |
HashSet(int initialCapacity) | Empty set with the given number of buckets. |
HashSet(int initialCapacity, float loadFactor) | Empty set with the given capacity and load factor. |
HashSet(Collection<? extends E> c) | A set containing the unique elements of another collection. |
Java 19 also added HashSet.newHashSet(int numElements), which picks a capacity large enough to hold that many elements without resizing.
5. Adding, Removing and Iterating
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
public class HashSetOperations {
public static void main(String[] args) {
Set<Integer> ids = new HashSet<>(List.of(101, 205, 307, 410, 512));
ids.add(615);
System.out.println("After add: " + ids.size() + " ids");
System.out.println("remove(205): " + ids.remove(205));
System.out.println("remove(999): " + ids.remove(999));
ids.removeIf(id -> id > 500);
System.out.println("After removeIf: " + new TreeSet<>(ids) + " (sorted for display)");
int sum = 0;
for (int id : ids) sum += id; // loop over every element
System.out.println("Sum of ids: " + sum);
ids.clear();
System.out.println("isEmpty(): " + ids.isEmpty());
}
}
After add: 6 ids remove(205): true remove(999): false After removeIf: [101, 307, 410] (sorted for display) Sum of ids: 818 isEmpty(): true
6. How HashSet Works Internally
Inside every HashSet is a HashMap. When you call set.add(e), the set calls map.put(e, PRESENT), where PRESENT is one shared dummy object. Adding an element therefore follows the same steps as adding a key to a HashMap:
- Hash: call
e.hashCode()and mix its bits. - Bucket: turn the hash into an index in the internal array (the bucket).
- Compare: if the bucket already holds elements, compare with
equals(). If one is equal, the new element is a duplicate and is not added. - Store: otherwise, add the element to the bucket.
This program shows the bucket calculation for a table with 16 buckets:
public class HashSetBuckets {
// The same calculation HashMap uses to choose a bucket
static int bucket(Object key, int tableSize) {
int h = key.hashCode();
h = h ^ (h >>> 16); // mix the high bits into the low bits
return h & (tableSize - 1); // same as h % tableSize for powers of two
}
public static void main(String[] args) {
String[] words = {"apple", "banana", "cherry", "date", "fig"};
for (String w : words) {
System.out.printf("%-7s hashCode=%-11d bucket (of 16) = %d%n", w, w.hashCode(), bucket(w, 16));
}
}
}
apple hashCode=93029210 bucket (of 16) = 1 banana hashCode=-1396355227 bucket (of 16) = 0 cherry hashCode=-1361513063 bucket (of 16) = 1 date hashCode=3076014 bucket (of 16) = 0 fig hashCode=101380 bucket (of 16) = 5
Several words land in the same bucket; this is called a collision. A bucket stores its elements as a small linked list, and since Java 8 it turns into a balanced tree when it gets crowded (more than 8 entries in a table of at least 64 buckets), so even bad hash codes cannot make lookups worse than O(log n).
7. Capacity, Load Factor and Rehashing
- Capacity is the number of buckets (16 by default, always a power of two).
- Load factor is how full the table may get before it grows (0.75 by default).
- When
size > capacity × loadFactor(12 elements for the defaults), the table doubles and every element is moved to its new bucket. This is called rehashing.
If you know you will store about 10,000 elements, create the set with enough capacity to skip repeated rehashing, for example HashSet.newHashSet(10_000) on Java 19+, or new HashSet<>(13_334) on older versions (10,000 / 0.75).
8. Storing Your Own Objects
For HashSet to recognise duplicates of your own class, override equals() and hashCode() using the same fields. Here, two employees with the same id count as the same person:
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
public class HashSetEmployees {
static final class Employee {
private final int id;
private final String name;
Employee(int id, String name) { this.id = id; this.name = name; }
// Two employees are the same person if they have the same id
@Override public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Employee e)) return false;
return id == e.id;
}
@Override public int hashCode() { return Objects.hash(id); }
@Override public String toString() { return id + ":" + name; }
}
public static void main(String[] args) {
Set<Employee> staff = new HashSet<>();
staff.add(new Employee(1, "Asha"));
staff.add(new Employee(2, "Ravi"));
boolean added = staff.add(new Employee(1, "Asha K.")); // same id
System.out.println("Second Asha added? " + added);
System.out.println("Staff size: " + staff.size());
System.out.println("Has id 2? " + staff.contains(new Employee(2, "anyone")));
}
}
Second Asha added? false Staff size: 2 Has id 2? true
a.equals(b) is true, then a.hashCode() == b.hashCode() must be true. The opposite is not required: different objects may share a hash code (a collision), which is legal but slows lookups if it happens a lot.9. Never Change an Element Inside a HashSet
The bucket is chosen from the hash code at the moment the element is added. If you later change a field used by hashCode(), the element sits in the wrong bucket and the set can no longer find it:
import java.util.HashSet;
import java.util.Set;
public class MutableKeyProblem {
static class Box {
int size;
Box(int size) { this.size = size; }
@Override public boolean equals(Object o) { return o instanceof Box b && b.size == size; }
@Override public int hashCode() { return Integer.hashCode(size); }
}
public static void main(String[] args) {
Set<Box> boxes = new HashSet<>();
Box b = new Box(10);
boxes.add(b);
b.size = 20; // changes the hash code!
System.out.println("contains(b): " + boxes.contains(b));
System.out.println("remove(b): " + boxes.remove(b));
System.out.println("size: " + boxes.size());
}
}
contains(b): false remove(b): false size: 1
The set still holds one element, but it can neither find nor remove it. Use immutable objects (final fields, records, String, Integer) as set elements, or remove the element before changing it and add it back afterwards.
10. Common HashSet Methods
| Method | Description | Time |
|---|---|---|
add(e) | Adds the element if absent; returns false for a duplicate. | O(1) |
remove(o) | Removes the element; returns true if it was present. | O(1) |
contains(o) | Checks whether the element is present. | O(1) |
size(), isEmpty() | Count and empty check. | O(1) |
clear() | Removes all elements. | O(capacity) |
addAll, retainAll, removeAll | Union, intersection and difference. | O(n) |
removeIf(p) | Removes every matching element. | O(n) |
iterator(), forEach(), stream() | Visit the elements. | O(n + capacity) |
clone() | Shallow copy of the set. | O(n) |
11. HashSet and Thread Safety
HashSet is not synchronized. If several threads add to it at the same time, elements can be lost or the internal table can be corrupted. Use a concurrent set instead:
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentSetDemo {
public static void main(String[] args) throws InterruptedException {
Set<Integer> seen = ConcurrentHashMap.newKeySet(); // thread-safe set
Runnable task = () -> {
for (int i = 0; i < 10_000; i++) seen.add(i);
};
Thread t1 = new Thread(task), t2 = new Thread(task);
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println("Unique numbers: " + seen.size()); // always 10000
}
}
Unique numbers: 10000
Other options are Collections.synchronizedSet(new HashSet<>()), ConcurrentSkipListSet for a sorted set, and CopyOnWriteArraySet for small sets that are mostly read.
12. HashSet vs LinkedHashSet vs TreeSet
| Feature | HashSet | LinkedHashSet | TreeSet |
|---|---|---|---|
| Order | None | Insertion | Sorted |
| add / remove / contains | O(1) | O(1) | O(log n) |
| Backed by | HashMap | LinkedHashMap | TreeMap |
null | One | One | Not allowed |
| Duplicates detected by | hashCode() + equals() | hashCode() + equals() | compareTo() / Comparator |
| Memory | Lowest | Higher (links) | Higher (tree nodes) |
13. Best Practices
- Use HashSet when you need uniqueness or fast membership checks and do not care about order.
- Override
equals()andhashCode()together, or use records. - Keep elements immutable while they are in the set.
- Size the set up front when you know roughly how many elements it will hold.
- Use LinkedHashSet if users will see the elements and order matters.
- Use a concurrent set for data shared between threads.
14. Summary
- HashSet stores unique elements in a hash table backed by a HashMap.
- Basic operations are O(1) on average; iteration order is not guaranteed.
- Default capacity is 16 and load factor 0.75; the table doubles when it gets too full.
- Duplicates are detected with
hashCode()andequals(), which your classes must override consistently. - Changing an element's hash fields while it is in the set breaks lookups.
- It allows one
nulland is not thread-safe.
Related Tutorials
- Set Interface in Java
- LinkedHashSet in Java
- TreeSet in Java
- HashMap in Java
- Difference between HashSet and HashMap
Reference: Java SE 21 API: java.util.HashSet
Frequently Asked Questions (FAQ)
Q1. What is a HashSet in Java?
HashSet is a class in java.util that implements the Set interface using a hash table. It stores unique elements, allows one null, gives no ordering guarantee, and performs add, remove and contains in constant time on average.
Q2. How does HashSet work internally?
It wraps a HashMap. Each element becomes a key of that map with a shared dummy value. The element's hashCode() selects a bucket, and equals() checks for duplicates inside the bucket.
Q3. What is the default capacity and load factor of a HashSet?
The default capacity is 16 buckets and the default load factor is 0.75. The table doubles once the set holds more than 12 elements (16 × 0.75), then again at 24, 48 and so on.
Q4. Why does HashSet not maintain insertion order?
Elements are placed in buckets according to their hash codes, and iteration walks the buckets in index order. That order has nothing to do with when elements were added, and it can change after the table resizes. Use LinkedHashSet if you need insertion order.
Q5. How does HashSet prevent duplicate elements?
Before adding, it looks in the element's bucket for an existing element that has the same hash and is equals() to the new one. If it finds one, add() returns false and nothing changes.
Q6. Can HashSet store null?
Yes, one null element. Its hash code is treated as 0, so it always goes into bucket 0. Adding a second null is ignored like any other duplicate.
Q7. What happens if I don't override hashCode() in my class?
The default hashCode() is based on object identity, so two objects with the same data usually get different hash codes and land in different buckets. The set then stores both as separate elements, even if equals() says they are equal.
Q8. What is a hash collision?
When two different elements map to the same bucket. HashSet handles it by storing several elements in the bucket (as a linked list, or a red-black tree when the bucket grows large). Collisions are normal; many of them slow down lookups.
Q9. What is rehashing?
When the number of elements exceeds capacity × load factor, the HashSet creates a table twice as large and moves every element to its new bucket. It is an O(n) operation that happens rarely, so adding is still O(1) on average.
Q10. Is HashSet thread-safe?
No. Use ConcurrentHashMap.newKeySet(), Collections.synchronizedSet(new HashSet<>()), ConcurrentSkipListSet or CopyOnWriteArraySet for shared sets.
Q11. How do I get an element from a HashSet?
There is no get() method because sets have no positions. Use contains() to check for an element, loop over the set, or use a stream such as set.stream().filter(...).findFirst(). If you need to look up objects by a key, use a HashMap.
Q12. What is the time complexity of HashSet operations?
add, remove and contains are O(1) on average. In the worst case, with many collisions in one bucket, they are O(log n) since Java 8 (O(n) before). Iteration is O(n + capacity).
Q13. What is the difference between HashSet and HashMap?
A HashSet stores single values; a HashMap stores key–value pairs. HashSet is actually built on a HashMap: its elements are the map's keys. HashSet uses add(), while HashMap uses put(key, value).
Q14. What is the difference between HashSet and TreeSet?
HashSet is unordered and O(1); TreeSet is sorted and O(log n). HashSet uses hashCode()/equals() and allows null; TreeSet uses compareTo() or a Comparator and does not allow null.
Q15. Why is it dangerous to modify an object stored in a HashSet?
If the change affects hashCode(), the object remains in the bucket for its old hash. Lookups now go to a different bucket, so contains() and remove() fail, and the set can end up with apparent duplicates.
Q16. How do I sort a HashSet?
A HashSet itself cannot be sorted. Copy it into a TreeSet (new TreeSet<>(hashSet)) or into a list and sort the list:
List<String> sorted = new ArrayList<>(hashSet);
Collections.sort(sorted);
Q17. How do I choose the initial capacity of a HashSet?
Divide the expected number of elements by the load factor (0.75) and round up, or use HashSet.newHashSet(expectedSize) on Java 19 and later. This avoids repeated rehashing when filling a large set.
Q18. Is the HashSet iterator fail-fast?
Yes. If the set is changed structurally during iteration other than through the iterator's own remove(), the iterator throws ConcurrentModificationException.
Q19. How do I convert a HashSet to a List or an array?
List<String> list = new ArrayList<>(set);
String[] array = set.toArray(new String[0]);
Q20. Can HashSet contain elements of different types?
A raw or HashSet<Object> can, but that is rarely a good idea. Use a specific type parameter, such as HashSet<String>, so the compiler can check every element.