Java HashSet

The Java HashSet Class

The HashSet class is the most common implementation of the Set interface. It is backed by a hash table (actually a HashMap instance).


Key Characteristics of HashSet


How HashSet Works

When you add an object to a HashSet, it calculates the object's hash code using its hashCode() method. This hash code is an integer that determines the "bucket" or location where the object should be stored in the underlying hash table.

When you check if an object contains() in the set, it first calculates the hash code to quickly find the right bucket, and then it uses the equals() method to check for an exact match among the few elements in that bucket.

Important: If you store custom objects in a HashSet, you must properly implement both the hashCode() and equals() methods in your class. If you don't, the HashSet will not be able to correctly identify duplicate objects.


When to Use HashSet

HashSet is the ideal choice when you need a collection that:

  1. Guarantees uniqueness of elements.
  2. Does not require the elements to be stored in any particular order.
  3. Requires high-speed access, insertion, and deletion.

It's perfect for tasks like finding unique items in a list or quickly checking for the existence of an item.


HashSet Example

Here is an example demonstrating common operations on a HashSet.

Working with `HashSet`

import java.util.HashSet;

public class Main { public static void main(String[] args) { HashSet<String> cars = new HashSet<String>(); // Add items cars.add("Volvo"); cars.add("BMW"); cars.add("Ford"); cars.add("BMW"); // This will be ignored cars.add("Mazda"); System.out.println(cars); // Note: The order is not guaranteed // Check if an item exists System.out.println("Contains 'Mazda': " + cars.contains("Mazda")); // Remove an item cars.remove("Volvo"); System.out.println("After removing Volvo: " + cars); // Loop through a HashSet System.out.println("--- Looping through cars ---"); for (String car : cars) { System.out.println(car); } } }


Advanced: Load Factor and Initial Capacity

When you create a HashSet, it initializes an underlying hash table. By default, it has an initial capacity of 16 and a "load factor" of 0.75.

The load factor is a measure of how full the hash table is allowed to get before its capacity is automatically increased. A load factor of 0.75 means the set will double its capacity when it becomes 75% full.

If you know in advance how many elements you will store, you can set the initial capacity to avoid costly resizing operations.

Tuning a HashSet

import java.util.HashSet;
import java.util.Set;

public class Main { public static void main(String[] args) { // Initial capacity of 100, load factor of 0.8 // Optimized for storing around 80 items without resizing Set<String> optimizedSet = new HashSet<>(100, 0.8f); optimizedSet.add("Efficient"); System.out.println(optimizedSet); } }


Advanced: Removing Elements Conditionally (Java 8+)

Java 8 introduced the removeIf() method, which allows you to remove elements that match a certain condition (a Predicate) without needing to manually write an Iterator loop.

Using `removeIf()`

import java.util.HashSet;
import java.util.Set;
import java.util.Arrays;

public class Main { public static void main(String[] args) { Set<Integer> numbers = new HashSet<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8)); // Remove all even numbers using a lambda expression numbers.removeIf(n -> n % 2 == 0); System.out.println("Odd numbers only: " + numbers); } }


Exercise

?

What method must be properly implemented in your class to store objects in a HashSet correctly?