equals() and hashCode() in Java

Two objects that are equal must have the same hash code. Breaking that contract quietly breaks every hash based collection.

Two kinds of equality

==equals
ComparesReferences, or primitive valuesWhatever the class defines
OverridableNoYes
Default behaviourIdentityIdentity, inherited from Object

Identity asks "is this the same object". Equality asks "do these represent the same value". For a Money or a Point, the second question is the useful one.

The equals contract

For any non null references, equals must be:

  • Reflexive - a.equals(a) is true.
  • Symmetric - if a.equals(b) then b.equals(a).
  • Transitive - if a.equals(b) and b.equals(c) then a.equals(c).
  • Consistent - repeated calls give the same answer while the objects are unchanged.
  • Null safe - a.equals(null) is false, never an exception.

The hashCode contract

  • Equal objects must return the same hash code.
  • Unequal objects may return the same hash code, which is a collision and is allowed.
  • The value must not change while the object is in a hash based collection.
Only the first rule is enforced by consequences rather than by the compiler. Break it and a HashMap will store an entry it can never find again.

Writing them correctly

import java.util.Objects;

public final class Money {

    private final String currency;
    private final long minorUnits;

    public Money(String currency, long minorUnits) {
        this.currency = Objects.requireNonNull(currency);
        this.minorUnits = minorUnits;
    }

    @Override
    public boolean equals(Object other) {
        if (this == other) {
            return true;                       // fast path
        }
        if (!(other instanceof Money money)) {
            return false;                      // also handles null
        }
        return minorUnits == money.minorUnits
                && currency.equals(money.currency);
    }

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

    @Override
    public String toString() {
        return currency + " " + minorUnits;
    }
}

Use exactly the same fields in both methods. That is the simplest way to keep the contract true.

What goes wrong without hashCode

// Money overrides equals but NOT hashCode
Set<Money> prices = new HashSet<>();
prices.add(new Money("INR", 500));

System.out.println(prices.contains(new Money("INR", 500)));   // false

A HashSet first uses the hash code to find a bucket. The two objects produce different hashes, land in different buckets, and equals is never even consulted. The entry is present but unreachable.

bucket 12 -> [ Money INR 500 ]     stored here from hash 12
lookup with a new object -> hash 87 -> bucket 87 is empty -> not found

Mutable keys are a trap

class MutableKey {
    String name;    // used by equals and hashCode
}

MutableKey key = new MutableKey();
key.name = "first";

Map<MutableKey, String> map = new HashMap<>();
map.put(key, "value");

key.name = "second";              // the hash changed
System.out.println(map.get(key)); // null - it is in the old bucket

This is why String, Integer and LocalDate are good keys and mutable objects are not.

Records do it for you

public record Money(String currency, long minorUnits) { }

A record generates equals, hashCode and toString from all its components, and they are consistent by construction. For a pure value type this is the best option available.

Inheritance and symmetry

class Point {
    int x, y;
    @Override public boolean equals(Object o) {
        return o instanceof Point p && p.x == x && p.y == y;
    }
}

class ColouredPoint extends Point {
    String colour;
    @Override public boolean equals(Object o) {
        return o instanceof ColouredPoint c && super.equals(o) && c.colour.equals(colour);
    }
}

Point p = new Point();
ColouredPoint c = new ColouredPoint();
// p.equals(c) can be true while c.equals(p) is false -> symmetry is broken

There is no fully satisfactory way to extend a value class and keep the contract. The practical answers are to make value classes final, or to use composition instead of inheritance.

Common mistakes

  • Overriding equals without hashCode.
  • Declaring equals(MyType other), which overloads and never overrides.
  • Using mutable fields in equals and then mutating them while the object is a key.
  • Comparing floating point fields directly rather than with Double.compare.
  • Using getClass() != o.getClass() or instanceof without understanding the inheritance trade off.
  • Returning a constant from hashCode. It is technically legal and turns a map into a linked list.

Best practices

  • Use a record for value types and let the compiler generate all three methods.
  • Otherwise use Objects.equals for fields and Objects.hash for the hash.
  • Use the same, immutable fields in both methods.
  • Start equals with the this == other fast path.
  • Make value classes final.

Practice

  1. Write a Book class with ISBN based equality and confirm a HashSet rejects a duplicate.
  2. Remove hashCode from it and explain the exact step at which lookup fails.
  3. Why is a.equals(null) required to return false rather than throw?
  4. Show how mutating a key field after insertion loses the entry.
  5. Convert the class to a record and list what the compiler now guarantees.

Conclusion

Override the pair together, build both from the same immutable fields, and prefer records when the class is a value. The contract is simple, and hash based collections depend on it completely.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Java notes →

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.