DEV Community

Cover image for Demystifying hashCode() and equals(): The Backbone of Java Hash Collections
Arshi Saxena
Arshi Saxena

Posted on

Demystifying hashCode() and equals(): The Backbone of Java Hash Collections

When working with Java's hash-based collections like HashMap and HashSet, the methods hashCode() and equals() play a critical role. Understanding their interplay can help you avoid subtle bugs and optimize object comparison.


Introduction

While determining the equality of primitive values is straightforward, comparing custom objects requires additional guidance.

For instance, collections like HashMap and HashSet rely on you to define how your custom objects should be differentiated. This is where hashCode() and equals() step in, providing the logic needed for these collections to function correctly.


How Elements Are Stored in a HashMap

To understand why hashCode() and equals() are important, let’s first explore how HashMap stores elements under the hood:

1. Array of Buckets:
A HashMap internally maintains an array where each slot (or bucket) holds one or more entries. These entries are key-value pairs.

2. Index Calculation Using hashCode():

  • When you add a key to the map, its hashCode() method computes a numeric hash value.

  • This value determines the index of the bucket where the key-value pair will be stored.

3. Handling Collisions:

  • If multiple keys produce the same bucket index (due to a hash collision), they are stored as a linked list or a balanced tree (depending on Java version).

  • Within the bucket, the equals() method distinguishes between the keys.

Visualization

Let’s imagine adding three keys to a HashMap where two keys collide:

Bucket Index Content
0 [Key1, Value1]
1 [Key2, Value2] → [Key3, Value3]
  • Key1 is stored in Bucket 0.

  • Key2 and Key3 are stored in Bucket 1 because they have the same hash index. The equals() method differentiates them.


Why hashCode() and equals() Matter

Java collections like HashMap and HashSet rely on these methods to:

  1. Store objects efficiently.
  2. Determine equality for lookups and retrievals.

Failing to implement these methods correctly can lead to:

  • Duplicate entries: When logically identical objects are treated as distinct.
  • Unexpected behaviors: Keys failing to retrieve values.

How They Work Together

1. hashCode() Determines the Bucket

  • A hashCode() generates a numeric hash for an object.

  • This hash value determines the bucket where the object will be stored in a hash table.

2. equals() Resolves Collisions Within the Bucket

  • If two objects have the same hash code, they end up in the same bucket (collision).

  • The equals() method is then used to identify the specific object.


The Contract Between hashCode() and equals()

The relationship between hashCode() and equals() is governed by a strict contract to ensure consistent behavior in hash-based collections:

1. If two objects are equal according to equals(), they must have the same hashCode().

  • This ensures that logically identical objects are stored in the same bucket.

2. Two objects with the same hashCode() are not guaranteed to be equal.

  • This is known as a hash collision, where different objects map to the same hash code.

  • In such cases, the equals() method is used to determine whether the objects are genuinely equal.

Remember: Breaking this contract can lead to unpredictable behavior in collections like HashMap and HashSet, such as duplicate entries or lookup failures.


Implementation Example

Let’s implement hashCode() and equals() for a custom Person class:

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

class Person {
    private final String name; // final ensures immutability
    private final int id; // final ensures immutability

    Person(String name, int id) {
        this.name = name;
        this.id = id;
    }

    @Override
    public int hashCode() {
        // Use Objects.hash() to compute hashCode based on the fields
        /* Benefit: 
         * > This eliminates the need to manually compute the hash code,
         *   and it also handles null values 
         * > If any of the fields are null, it won't throw a
         *   NullPointerException
         * */
        // Using both 'id' and 'name' for hash value
        return Objects.hash(id, name);
    }

    @Override
    public boolean equals(Object o) {
        // Checking whether the two references point to the same memory
        // If true, it implies that they are the exact same object
        if (this == o) return true;

        // Checking type to see if the classes are derivative of each other
        if (o == null || getClass() != o.getClass()) return false;
        // Type-casting
        Person person = (Person) o;

        // Use Objects.equals() for null-safe 'name' comparison
        /* > The Objects.equals() method compares the name fields of two
         *   Person objects, safely handling null values (returns true
         *   if both are null, or if both are non-null and equal) 
         * > Benefit: This reduces the need for manual null checks and
         *   simplifies the comparison process
         * */
        return id == person.id && Objects.equals(name, person.name);
    }
}

public class HashcodeEqualContract {
    public static void main(String[] args) {
        HashMap<Person, String> map = new HashMap<>();

        Person p1 = new Person("Alice", 123);
        Person p2 = new Person("Bob", 123);
        Person p3 = new Person("Alice", 123);

        System.out.println(p1.equals(p3)); // true

        map.put(p1, "Engineer");
        map.put(p2, "Doctor");
        map.put(p3, "Scientist");

        System.out.println(map.size()); // 2
        System.out.println(map.get(p1)); // Scientist
        System.out.println(map.get(p2)); // Doctor
    }
}
Enter fullscreen mode Exit fullscreen mode

What’s Happening in the Code?

  1. Bucket Assignment:
    The hashCode() method places p1 and p3 in the same bucket because their hash codes are identical.

  2. Equality Check:
    The equals() method confirms that p1 and p3 are logically equal. Hence, the value for p1 is overridden by p3.

  3. Distinct Keys:
    p2 has the same id as p1 but a different name. Therefore, it’s stored in a separate bucket.


Best Practices and Takeaways

  • Always override both hashCode() and equals().

    • Only overriding one of them can lead to inconsistent behavior in collections.
  • Rely on immutable fields when implementing these methods.

    • Changes to mutable fields used in equality or hash computations can disrupt collections.
  • Use Objects.hash() and Objects.equals().

    • These utility methods simplify implementation and handle null values gracefully.

Key Takeaways

  • hashCode() places objects in buckets; equals() distinguishes them.

  • Correct implementation ensures consistent behavior in collections like HashMap and HashSet.

  • Using best practices makes your code robust and less error-prone.

By understanding and implementing the hashCode() and equals() methods correctly, you can leverage the full power of hash-based collections in Java.


Related Posts

Happy Coding!

Top comments (0)