Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Fixing Java equals and hashCode Contract Violations

Hello! This first module focuses on Java decisions that can produce subtle backend bugs even when code compiles and ordinary tests pass. We’ll begin with equals() and hashCode(): the rules behind value-based equality, how a HashMap uses them, and how to diagnose a key that cannot reliably be found after insertion.

This lesson is designed for roughly 40 minutes. The goal is to leave with an interview-ready explanation and a repeatable way to inspect the code.


Define equality before writing the methods

Consider a key used to look up a customer within a tenant. Two key objects should represent the same customer when both their tenant IDs and customer IDs match. Whether they are the same Java object is irrelevant.

Without an override, Object.equals() tests object identity: two separately constructed keys compare unequal. An override can instead express the domain rule. But it must obey Java’s equality contract, and its hashCode() must agree with that rule.

Read the authoritative definitions before looking at an implementation:

Object (Java SE 22 & JDK 22)

Read the Object API documentation from Oracle for the precise contracts that custom keys must satisfy.

In the java.lang.Object documentation, find the hashCode method. Read its three rules, ending just before “API Note.” Then find the equals method and read its five rules, from the introductory sentence through the null rule. Focus particularly on what must follow when two objects are equal—and what need not follow when they are unequal.

For non-null objects, equals() must be reflexive, symmetric, transitive, and consistent while the information it compares remains unchanged. It must also return false for null. The link to hashing is one-way:

The reverse is not required. Unequal objects may share a hash code; that is a collision, not a contract violation. Likewise, a hash code need not remain the same across application runs. It must remain consistent during a run while the information used for equality is unchanged.


Trace a failed map lookup

A HashMap uses a key’s hash code to narrow its search to a location in its internal table. It then checks candidate keys for equality. The diagram is a useful simplified picture: several keys can reach one bucket, where equality distinguishes entries. Real HashMap internals may use structures other than the linked chains shown.

Keys are mapped to positions in a bucket array using their hash codes; when entries contend for a location, key comparisons distinguish them.

Watch a short demonstration of the lookup failure. Treat any discussion of a default hash code as implementation detail, not a promise that it is literally a memory address or unique for every object.

The equals hashCode Contract - Java Programming

Watch “The equals hashCode Contract - Java Programming” by Will Tollefson for a map-based demonstration of why matching field values are insufficient when the two methods disagree.

Watch the map demonstration, from construction of the map through the corrected field-based hash code. Then watch the consistency failure, where a changing hash code makes retrieval unreliable. Focus on the hash code used during lookup, not just the one used during insertion.

Here is a deterministic version of the bug. displayName is descriptive data; the declared equality rule uses only the two IDs.

import java.util.Objects;

final class BrokenCustomerKey {
    private final String tenantId;
    private final String customerId;
    private final String displayName;

    BrokenCustomerKey(String tenantId, String customerId, String displayName) {
        this.tenantId = tenantId;
        this.customerId = customerId;
        this.displayName = displayName;
    }

    @Override
    public boolean equals(Object other) {
        if (this == other) return true;
        if (!(other instanceof BrokenCustomerKey)) return false;
        BrokenCustomerKey that = (BrokenCustomerKey) other;
        return Objects.equals(tenantId, that.tenantId)
                && Objects.equals(customerId, that.customerId);
    }

    @Override
    public int hashCode() {
        return Objects.hash(tenantId, customerId, displayName); // Bug
    }
}

Compare new BrokenCustomerKey("t1", "c7", "A") with new BrokenCustomerKey("t1", "c7", "B"). equals() returns true, but the differing names produce differing hash codes. A map lookup using the second object will not find an entry inserted with the first: the stored entry has a different hash. Insertion can similarly leave the map with two entries that your equals() definition calls equal.

A closely related mistake is overriding equals() but not hashCode(). The class then inherits Object.hashCode(), which is not designed to produce matching hashes for separately constructed, value-equal instances. The failure might not appear in every small run, but the contract is broken.

The correction starts with the equality rule, not with an attempt to make every hash unique:

import java.util.Objects;

public final class CustomerKey {
    private final String tenantId;
    private final String customerId;

    public CustomerKey(String tenantId, String customerId) {
        this.tenantId = tenantId;
        this.customerId = customerId;
    }

    @Override
    public boolean equals(Object other) {
        if (this == other) return true;
        if (!(other instanceof CustomerKey)) return false;
        CustomerKey that = (CustomerKey) other;
        return Objects.equals(tenantId, that.tenantId)
                && Objects.equals(customerId, that.customerId);
    }

    @Override
    public int hashCode() {
        return Objects.hash(tenantId, customerId);
    }
}

displayName does not belong in this key; it can live in the associated customer data. Both methods now use the same identity-defining fields. Omitting a field from hashCode() that equals() uses is not automatically a contract violation—it may merely cause more collisions. Including a field that can differ between equal objects is a violation.


Check the two less obvious failure modes

Mutation after insertion. Even a correct pair of methods cannot make a mutable key safe to change while it is in a hash-based collection. For example, a Java list compares and hashes its contents. If a list used as a map key is changed after insertion, its current hash may lead a lookup away from the location where the entry was stored. Both the original contents and the changed key can then fail to retrieve it reliably. For a key class, prefer immutable equality fields; final fields help, but a final reference to a mutable object would still need care. Defensive copying is the subject of the next lesson.

Inheritance and asymmetric equality. Suppose a base Money class accepts any instanceof Money with matching amount and currency, while a Voucher extends Money accepts only vouchers with a matching store. The base object can consider the voucher equal while the voucher rejects the base object. That violates symmetry before hashing even enters the discussion.

Java equals() and hashCode() Contracts

Read Baeldung’s Money and WrongVoucher example to see the inheritance failure with concrete objects, followed by a composition-based repair.

Find subsection “2.3. Violating equals() Symmetry With Inheritance.” Start at its opening question about how the contract can be violated; read the class example and the comparison of cash.equals(voucher) with voucher.equals(cash). Continue into subsection “2.4. Fixing equals() Symmetry With Composition” through the Voucher class code. Notice that the repaired voucher contains a Money value rather than claiming to be a subtype of Money.

Composition is not a universal requirement for every equality implementation. It is a clean repair here because Money and Voucher have different equality-defining information. Making CustomerKey final similarly prevents a subclass from adding an equality rule that conflicts with this one.


Turn the contract into a regression check

In an interview, a useful diagnostic order is: state which fields define equality; check both comparison directions; compare hashes of equal objects; then test retrieval with a separately constructed equal key. Using the exact same reference for both insertion and lookup can conceal a bad implementation.

These JUnit 5 checks capture the important behavior of the corrected CustomerKey:

import static org.junit.jupiter.api.Assertions.*;

import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;

class CustomerKeyTest {
    @Test
    void equalKeysHaveEqualHashesAndRetrieveTheSameEntry() {
        CustomerKey first = new CustomerKey("t1", "c7");
        CustomerKey sameCustomer = new CustomerKey("t1", "c7");

        assertEquals(first, first);
        assertEquals(first, sameCustomer);
        assertEquals(sameCustomer, first);
        assertEquals(first.hashCode(), sameCustomer.hashCode());
        assertNotEquals(first, null);

        Map<CustomerKey, String> customers = new HashMap<>();
        customers.put(first, "active");
        assertEquals("active", customers.get(sameCustomer));
    }

    @Test
    void bothIdsParticipateInEquality() {
        CustomerKey key = new CustomerKey("t1", "c7");

        assertNotEquals(key, new CustomerKey("t2", "c7"));
        assertNotEquals(key, new CustomerKey("t1", "c8"));
    }
}

These tests are valuable, but two examples do not prove symmetry or transitivity for every possible object—especially across a class hierarchy. When reviewing unfamiliar code, also inspect for an accidental overload such as equals(CustomerKey other) instead of equals(Object other). The @Override annotation helps catch that mistake at compile time.


Key takeaways

A reliable Java collection key has a clear equality rule, an equals(Object) implementation that satisfies the full contract, and a hashCode() consistent with that rule. Equal keys must have equal hashes; unequal keys need not have different hashes. When a map lookup fails unexpectedly, inspect the key’s methods, its equality fields, and whether any of those fields changed after insertion.

Next, we’ll build an immutable value object and use defensive copying where final references alone do not protect its state.

Can't find a good explanation? Sign up and we'll make it for you

Sign up