Blog

equals() and hashCode() in Java

Key Takeaways

  • == compares reference identity. An overridden equals(Object) can compare the logical state of distinct objects.
  • Equal objects must have equal hash codes, but unequal objects may share a hash code. A hash code is not a unique identifier.
  • Keep equality-relevant state unchanged while an object is a HashMap key or HashSet element. Prefer immutable value objects.
  • Records can generate component-based equality and hashing, but they don't make mutable component objects immutable.

Java equals() defines when objects count as equal; hashCode() supplies an integer used by hash-based collections to narrow their search. For a value object—an object defined by its contents rather than its identity—implement logical equality and a matching hash code so distinct, equal-valued instances work interchangeably as keys.

Java equals(): identity versus logical equality

Object supplies an identity-based implementation of equals(Object): it returns true only when both references refer to the same object. That's reference identity, not a Java-level promise about comparing literal memory addresses. Its behavior is equivalent to this method fragment:

// Fragment: behavior of Object.equals(Object).
public boolean equals(Object obj) {
    return this == obj;
}

Identity is appropriate when two separately created objects should remain distinct. But two Payment values representing the same currency and amount should compare equal in our example, even when created with separate constructor calls. That requires a value-based override rather than the inherited identity comparison.

The equals contract

For non-null references, the contract requires the following properties.

  • Reflexive: x.equals(x) is true.
  • Symmetric: x.equals(y) and y.equals(x) agree.
  • Transitive: if x.equals(y) and y.equals(z), then x.equals(z).
  • Consistent: repeated comparisons agree while the information used by equality remains unchanged.
  • Null-safe: x.equals(null) is false.

Java hashCode(): the matching contract

hashCode() returns an int. Its contract establishes three rules.

  • Equal objects must return the same hash code.
  • Unequal objects may return the same hash code—a collision. Better distribution can improve hash-table performance.
  • During one application execution, an object's hash must stay consistent while information used by equals is unchanged. It need not remain stable across application executions.

Overriding equals doesn't make Java automatically call hashCode during a direct comparison. The requirement is that the resulting implementations agree with the contract; you don't need handwritten overrides when inherited or generated methods already agree. For a class that changes identity equality into value equality, implement a matching hash code rather than leaving future collection behavior to chance.

A complete Payment equals and hashCode example

This example defines a payment value by a case-sensitive currency label and an amount in integer minor units. Under our USD convention, 5000 represents USD 50.00. This is a value example, not a transaction identity model: two real transactions may need distinct transaction IDs even when their amounts match.

Save this as Payment.java. The constructor rejects a null currency; it deliberately leaves currency-format validation to your application's boundary. Both fields are final, and String is immutable, so the equality state cannot change after construction.

import java.util.Objects;

public final class Payment {
    private final String currency;
    private final long amountMinor;

    public Payment(String currency, long amountMinor) {
        this.currency = Objects.requireNonNull(currency, "currency");
        this.amountMinor = amountMinor;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        Payment other = (Payment) obj;
        return amountMinor == other.amountMinor
                && currency.equals(other.currency);
    }

    @Override
    public int hashCode() {
        return Objects.hash(currency, amountMinor);
    }
}

The initial this == obj check is a fast path for self-comparison. After rejecting null and incompatible classes, the method compares the two fields that define this value; hashing uses those same fields.

Override equals(Object), not equals(Payment)

public boolean equals(Payment other) is an overload, not an override of equals(Object). It leaves the inherited Object-parameter method in place. Always add @Override: the compiler catches this signature mistake, as the language specification's equality example demonstrates.

Why this example uses getClass()

getClass() enforces exact-runtime-class equality. An instanceof Payment check would accept subclasses, but this Payment class is final, so either approach accepts the same types here.

Inheritance makes the decision harder. If a base class accepts a subclass as equal while the subclass insists on comparing an extra field, symmetry can fail. Treat cross-class equality as a deliberate design decision; for ordinary value classes, making the class final avoids that problem.

Money and floating-point equality need an explicit policy

Prefer integer minor units or BigDecimal for monetary values rather than binary floating-point amounts. Many decimal fractions cannot be represented exactly as double; integer units also require you to define the unit for each supported currency rather than assuming every currency uses cents.

If your domain genuinely needs a double field, Double.compare(a, b) == 0 treats NaN values as equal and distinguishes positive zero from negative zero. Pair those semantics with Double.hashCode(value); don't assume they are identical to primitive ==.

BigDecimal has a different trap: equals compares both value and scale, so 2.0 and 2.00 are unequal, although compareTo returns zero. If your domain ignores scale, normalize amounts consistently before comparing and hashing; don't combine scale-insensitive comparison with unnormalized BigDecimal.hashCode().

How HashMap uses equals and hashCode

A hash narrows the search; equality identifies the key. Conceptually, lookup follows this sequence. OpenJDK's implementation also checks stored hashes before comparing candidate keys, and can accept an identical reference without invoking equals.

lookup key
    |
    v
hashCode() -> hash spreading -> bucket selection
                                   |
                                   v
                     candidates with matching hash
                                   |
                                   v
                       same reference or equals()?
                          /                 \
                        yes                 no match
                         |                     |
                    return value           return null

Collisions don't imply equality: multiple unequal keys can share a hash and still coexist. Conversely, equality cannot rescue a key that hash-based lookup never considers. For the underlying storage mechanics, see our Java HashMap implementation guide.

Look up a Payment with a distinct but equal key

Save this as PaymentDemo.java, alongside the complete class above. The first two lines distinguish identity from value equality; the final lookup retrieves Sam using a newly constructed key.

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

public class PaymentDemo {
    public static void main(String[] args) {
        Payment payment1 = new Payment("USD", 5000);
        Payment payment2 = new Payment("USD", 5000);

        System.out.println(payment1 == payment2);      // false
        System.out.println(payment1.equals(payment2)); // true

        Map<Payment, String> owners = new HashMap<>();
        owners.put(new Payment("USD", 4300), "Sam");
        System.out.println(owners.get(new Payment("USD", 4300))); // Sam
    }
}

Compile the files and run the demo with the standard JDK tools.

javac Payment.java PaymentDemo.java
java PaymentDemo

For additional insertion, retrieval, and update patterns, see our Java HashMap examples.

What breaks when equal keys have different hashes?

Leaving identity-based hashing after introducing value equality can make lookup fail. It does not guarantee failure for every pair: identity hash codes can collide. To demonstrate the defect deterministically, this deliberately invalid key class returns controlled, unequal hashes for equal values.

import java.util.HashMap;

public class BrokenKeyDemo {
    // Intentionally violates the equals/hashCode contract.
    static final class BadKey {
        private final int value;
        private final int forcedHash;

        BadKey(int value, int forcedHash) {
            this.value = value;
            this.forcedHash = forcedHash;
        }

        @Override
        public boolean equals(Object obj) {
            return obj instanceof BadKey other && value == other.value;
        }

        @Override
        public int hashCode() {
            return forcedHash;
        }
    }

    public static void main(String[] args) {
        BadKey stored = new BadKey(43, 1);
        BadKey lookup = new BadKey(43, 2);
        var map = new HashMap<BadKey, String>(16);
        map.put(stored, "Sam");

        System.out.println(stored.equals(lookup)); // true
        System.out.println(map.get(lookup));      // null on OpenJDK HashMap
    }
}

For this example, OpenJDK selects different buckets for hashes 1 and 2, so the lookup never compares the stored key with the equal lookup key. That's an implementation demonstration, not a promise about arbitrary maps with contract-breaking keys; you can inspect the OpenJDK HashMap source for the lookup checks.

Objects.hash versus a direct hash implementation

Objects.hash(Object... values) is convenient for combining multiple fields. Calls with separate arguments can create a varargs Object[] and box primitive values, so it isn't automatically the best choice for a frequently executed method. Profile before changing a clear implementation.

A direct replacement for the method inside Payment avoids that varargs call and uses Long.hashCode(long) for the amount. It need not produce the same numeric result as Objects.hash; it needs to preserve the equality contract.

// Fragment: replacement method inside Payment.java.
@Override
public int hashCode() {
    return 31 * currency.hashCode() + Long.hashCode(amountMinor);
}

These are collection hashes, not cryptographic hashes or durable identifiers. Also note that Objects.hash(singleObject) generally differs from singleObject.hashCode(); use Objects.hashCode(singleObject) when you want the object's hash with null mapped to zero.

Mutable keys can break otherwise correct equality

A list's equality and hash depend on its elements. This standalone example changes those elements after insertion; on OpenJDK HashMap it prints Sam, then null, even though the lookup uses the same list reference.

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

public class MutableKeyDemo {
    public static void main(String[] args) {
        var key = new ArrayList<>(List.of("USD", "4300"));
        var owners = new HashMap<List<String>, String>();
        owners.put(key, "Sam");
        System.out.println(owners.get(key));

        key.set(1, "5000");
        System.out.println(owners.get(key));
    }
}

Prefer an immutable key such as Payment. If a legacy design requires changing equality state, remove the entry before the change and reinsert it afterward; don't wait until lookup is already broken. Apply the same stability rule to elements stored in HashSet.

Records can generate value equality

When all components define the value, a record can replace the handwritten class. Use this as an alternative Payment.java, not alongside the earlier class of the same name. Records generate equals and hashCode from their components unless you explicitly provide those methods.

import java.util.Objects;

public record Payment(String currency, long amountMinor) {
    public Payment {
        Objects.requireNonNull(currency, "currency");
    }
}

Generated equality requires the same record class and equal components. Reference components use Objects.equals semantics; primitive components use their corresponding wrapper comparison semantics, including the floating-point distinctions discussed above. Don't depend on a particular generated hash formula.

Records are only shallowly immutable: a final component reference can still point to a mutable list. An array component also doesn't automatically gain element-by-element equality. Choose immutable components or define defensive-copying and equality policies deliberately.

Test equality and collection behavior together

In a project already configured to run JUnit Jupiter tests, place this in PaymentTest.java under your test source directory, in the same package as Payment. The assertions cover the contract properties with representative values and exercise a real map lookup. JUnit's assertAll reports grouped assertion failures together.

import java.util.HashMap;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

class PaymentTest {
    @Test
    void equalityAndMapLookupAgree() {
        Payment a = new Payment("USD", 4300);
        Payment b = new Payment("USD", 4300);
        Payment c = new Payment("USD", 4300);
        var owners = new HashMap<Payment, String>();
        owners.put(a, "Sam");

        assertAll(
            () -> assertTrue(a.equals(a)),
            () -> assertTrue(a.equals(b)),
            () -> assertTrue(b.equals(a)),
            () -> assertTrue(b.equals(c)),
            () -> assertTrue(a.equals(c)),
            () -> assertEquals(a.equals(b), a.equals(b)),
            () -> assertFalse(a.equals(null)),
            () -> assertFalse(a.equals("USD 43.00")),
            () -> assertFalse(a.equals(new Payment("EUR", 4300))),
            () -> assertFalse(a.equals(new Payment("USD", 4301))),
            () -> assertEquals(a.hashCode(), b.hashCode()),
            () -> assertEquals("Sam", owners.get(b))
        );
    }
}

Representative tests aren't a proof for every possible state. Add cases for your own normalization, inheritance, nullability, and mutable-component policies. Don't assert that unequal objects must have different hashes: collisions are valid.

Common Mistakes

  • Choosing the wrong identity: decide whether you're modeling a value or a distinct entity before selecting equality fields.
  • Hashing unrelated changing state: a field that equality ignores must not make equal objects produce different hashes. Hashing fewer equality fields can be valid, but may increase collisions.
  • Confusing final references with immutable contents: lists and other referenced objects can still change underneath a key.
  • Persisting a collection hash as an ID: neither uniqueness nor cross-execution stability is part of the general contract.

FAQ

Why must equal Java objects have the same hash code?

Hash-based collections use hashes to narrow the candidates before checking equality. Equal objects with incompatible hashes can be missed during lookup; equal hashes alone don't establish equality.

What happens if I override equals but not hashCode?

Direct equals comparisons can still return true, but an incompatible inherited hashCode violates the contract and can break hash-based collection lookup. Failure isn't guaranteed for every pair because hash collisions are possible.

Why isn't HashMap.get returning the expected value?

Check for an equals overload instead of an override, inconsistent hashing, mutated key state, or equality fields that don't match your domain. HashMap also permits null values, so containsKey can distinguish an absent mapping from a mapping to null when the keys obey their contracts.

Should I use getClass() or instanceof in equals?

Use getClass() when equality should require the exact runtime class; instanceof also accepts compatible subclasses. For a final value class they accept the same types, while extensible classes need an explicit policy that preserves symmetry and transitivity.

Can I use a mutable object as a HashMap key?

Yes, provided its equality-relevant state remains unchanged while it is stored. Prefer an immutable key; if you must change that state, remove the entry before changing it and reinsert it afterward.

Further Reading

Written by Sam Erickson

Software engineer. I write every article on StackChief myself. Author of Mastering the Tech Interview. More about me