In Java, the hashCode() method, inherited from the Object class, plays a crucial role in the efficient functioning of hash-based collections like HashMap, HashSet, and Hashtable. It generates an integer hash code value for an object, which is used to determine the “bucket” where the object should be stored.

The hashCode() and equals() Contract

The most important concept to understand is the contract between hashCode() and equals():

  1. If two objects are equal according to the equals(Object) method, then calling the hashCode() method on each of the two objects must produce the same integer result.
  2. It is not required that if two objects are unequal according to the equals(Object) method, then calling the hashCode() method on each of the two objects must produce distinct integer results. However, producing distinct hash codes for unequal objects may improve the performance of hash tables.

In simple terms: equal objects must have equal hash codes.

If you override the equals() method, you must also override the hashCode() method to maintain this contract.

Why is This Contract So Important?

Hash-based collections use an object’s hash code to quickly locate it. Here’s a simplified view of how HashMap.put(key, value) works:

  1. It calculates the key’s hash code.
  2. It uses this hash code to find a bucket (an index in an internal array).
  3. If the bucket is empty, it stores the key-value pair there.
  4. If the bucket is not empty (a hash collision), it iterates through the existing entries in that bucket and uses the equals() method to check if the key already exists.

If you break the contract, these collections will behave incorrectly.

Example: Breaking the Contract

Let’s see what happens when we override equals() but not hashCode().

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

class Product {
    private String id;

    public Product(String id) {
        this.id = id;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Product product = (Product) o;
        return Objects.equals(id, product.id);
    }

    // hashCode() is NOT overridden
}

public class BrokenContractExample {
    public static void main(String[] args) {
        Map<Product, String> map = new HashMap<>();
        map.put(new Product("a"), "Apple");
        map.put(new Product("a"), "Avocado"); // Should replace "Apple"

        // The map size will be 2, which is incorrect!
        System.out.println("Map size: " + map.size());
        System.out.println(map);
    }
}

What went wrong?

  • We defined equals() to consider two Product objects with the same id as equal.
  • However, since we didn’t override hashCode(), each new Product("a") gets a different hash code from the default Object.hashCode() implementation (which is based on memory address).
  • The HashMap calculates two different hash codes, places the objects in two different buckets, and never even calls the equals() method. The result is a map with duplicate logical keys.

How to Correctly Implement hashCode()

To fix this, we must implement hashCode() based on the same fields used in the equals() method. The easiest way is to use the Objects.hash() utility method.

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

class CorrectProduct {
    private String id;

    public CorrectProduct(String id) {
        this.id = id;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        CorrectProduct that = (CorrectProduct) o;
        return Objects.equals(id, that.id);
    }

    @Override
    public int hashCode() {
        // Use the same field(s) as in equals()
        return Objects.hash(id);
    }
}

public class CorrectContractExample {
    public static void main(String[] args) {
        Map<CorrectProduct, String> map = new HashMap<>();
        map.put(new CorrectProduct("a"), "Apple");
        map.put(new CorrectProduct("a"), "Avocado"); // Correctly replaces "Apple"

        // The map size will be 1, which is correct.
        System.out.println("Map size: " + map.size());
        System.out.println(map);
    }
}

By correctly implementing both equals() and hashCode(), we ensure that hash-based collections work as expected, providing both correctness and performance.