00:00

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() and contains() are O(1) on average.
  • One null allowed.
  • 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

HashSet class hierarchy in Java
java.util.HashSet
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

HashSetQuickStart.java
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"));
    }
}
Output
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

ConstructorWhat 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

HashSetOperations.java
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());
    }
}
Output
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:

  1. Hash: call e.hashCode() and mix its bits.
  2. Bucket: turn the hash into an index in the internal array (the bucket).
  3. Compare: if the bucket already holds elements, compare with equals(). If one is equal, the new element is a duplicate and is not added.
  4. Store: otherwise, add the element to the bucket.

This program shows the bucket calculation for a table with 16 buckets:

HashSetBuckets.java
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));
        }
    }
}
Output
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:

HashSetEmployees.java
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")));
    }
}
Output
Second Asha added? false
Staff size:        2
Has id 2?          true
The contract: if 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:

MutableKeyProblem.java
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());
    }
}
Output
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

MethodDescriptionTime
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, removeAllUnion, 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:

ConcurrentSetDemo.java
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
    }
}
Output
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

FeatureHashSetLinkedHashSetTreeSet
OrderNoneInsertionSorted
add / remove / containsO(1)O(1)O(log n)
Backed byHashMapLinkedHashMapTreeMap
nullOneOneNot allowed
Duplicates detected byhashCode() + equals()hashCode() + equals()compareTo() / Comparator
MemoryLowestHigher (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() and hashCode() 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() and equals(), which your classes must override consistently.
  • Changing an element's hash fields while it is in the set breaks lookups.
  • It allows one null and is not thread-safe.

Related Tutorials

Reference: Java SE 21 API: java.util.HashSet