In Java, HashMap is a highly efficient structure for looking up data. Under the hood, it uses two core methods inherited from the `Object` class: hashCode() and equals(). If you modify one without adhering to the correct contract, you can break the HashMap, causing duplicate entries, memory leaks, and lookup failures.

In this guide, we will explain this essential Java contract using a broken mailbox analogy and walk through its execution logic step by step.

Illustration of a HashMap with colliding keys in bucket due to broken equals contract
Real-World Analogy: The Broken Mailbox Labeling

Imagine a school sorting room with numbered **mailboxes**. When a mail sorter gets a letter, they use two steps to deliver and retrieve letters:

  • Step 1: The Box Number (`hashCode()`): The sorter looks at the name and converts it to a number. For example, any name starting with "A" gets sorted into Mailbox 1. This is the **hashCode**.
  • Step 2: Checking the ID (`equals()`): If Mailbox 1 already has letters, the sorter must check the name on the envelope to find the correct letter. This is the **equals** check.

Now imagine we break the rules. We write a contract where:

  1. Every envelope we send gets placed in Mailbox 1 (hashCode() always returns 1).
  2. The ID check always returns **false** (equals() always says: "No, this is a different envelope").

When you send two letters addressed to "A", the sorter places the first letter in Mailbox 1. For the second letter, they go to Mailbox 1, check the name against the first letter, but because the equals check is broken, they think they are different and put the second letter in Mailbox 1 too. Mailbox 1 is now cluttered with duplicate letters, and you can never retrieve any letter because the sorter's identity check always says "Access Denied"!

Walkthrough of the Main Method Scenario

Let's trace how the program executes step-by-step from the entry point of the main method:

Step 1: Constructing the Objects

The program creates two distinct instances of the class TestTest, both having the internal name "A":

TestTest ob1 = new TestTest("A");
TestTest ob2 = new TestTest("A");

Even though they contain the same string value, they occupy different locations in memory.

Step 2: Putting items in the Map

Next, the program instantiates a HashMap and attempts to put both objects inside:

HashMap<TestTest, String> map = new HashMap<>();
map.put(ob1, ob1.getName());
map.put(ob2, ob2.getName());
  • First Put: The map evaluates ob1.hashCode(). The overridden method returns 1, so the entry is stored in bucket 1.
  • Second Put: The map evaluates ob2.hashCode(). It also returns 1. The map goes to bucket 1 and finds `ob1` already exists. It then calls ob2.equals(ob1). Because `equals()` is overridden to always return false, the map concludes they are different keys and adds `ob2` to bucket 1 as a new node, creating a **duplicate key**!
Step 3: Printing the duplicates

Finally, we print the Map representation:

System.out.println(map);

It prints: {TestTest{name='A'}='A', TestTest{name='A'}='A'}. The map has a size of 2, proving we have violated the unique-key guarantee of the Map interface.

Java Implementation

Below is the complete Java code demonstrating a broken contract leading to duplicated keys in a HashMap:

package io.practise.accolite;
 
import java.util.HashMap;
 
public class Main {
    public static void main(String[] args) {
        TestTest ob1 = new TestTest("A");
        TestTest ob2 = new TestTest("A");
 
        HashMap<TestTest, String> map = new HashMap<>();
 
        map.put(ob1, ob1.getName());
        map.put(ob2, ob2.getName());
 
        System.out.println(map);
    }
}
 
class TestTest {
    private String name;
 
    TestTest(String name) {
        this.name = name;
    }
 
    public String getName() {
        return name;
    }
 
    @Override
    public boolean equals(Object o) {
        // Warning: Violates reflexive/symmetric rules by always returning false
        return false;
    }
 
    @Override
    public int hashCode() {
        // Warning: All objects will collide inside bucket index 1
        return 1;
    }
 
    @Override
    public String toString() {
        return "TestTest{" + "name='" + name + '\'' + '}';
    }
}

Conclusion

The contract of hashCode() and equals() states that if two objects are equal according to the equals(Object) method, they must produce the same integer result from hashCode(). Furthermore, equals must be consistent and reflexive. Breaking this rule compromises the internal hashing buckets, causing maps and sets to behave unpredictably.