The contract between equals and hashCode — the most commonly broken Java rule.
Published September 21, 2026
Every Java object inherits two methods from Object that decide when two objects count as "the same":
equals(Object other) answers "do these two objects represent the same value?"hashCode() returns an int summarizing the object. Hash-based collections (HashMap, HashSet) use it to decide which bucket to look in, before ever calling equals.By default, both are based on identity: an object equals only itself, and two separate objects with identical fields are not equal. For value-like classes (a Money, an Email, a Point, a DTO used as a map key) that's wrong. You override both, and you must keep them consistent, or collections quietly stop working.
equals must be:
x.equals(x) is true.x.equals(y) ⇔ y.equals(x).x.equals(y) and y.equals(z), then x.equals(z).x.equals(null) is false, and never throws.And the rule that ties the two methods together:
If a.equals(b), then a.hashCode() == b.hashCode().
The reverse isn't required. Unequal objects may share a hash code, which is a collision, and it's legal, just slower. But equal objects with different hash codes break every hash-based collection.
equalsclass User {
private final String email;
User(String email) { this.email = email; }
@Override public boolean equals(Object o) {
return o instanceof User u && email.equals(u.email);
}
// hashCode() NOT overridden → still identity-based
}
Set<User> users = new HashSet<>();
users.add(new User("asha@example.com"));
users.contains(new User("asha@example.com")); // false! (and adding it again creates a duplicate)
A HashSet first uses hashCode() to pick a bucket. The two objects have different identity hash codes, so the lookup searches the wrong bucket and never calls equals at all. The objects are "equal", yet the set can't find the match.
public final class Money {
private final long amountInPaise;
private final String currency;
public Money(long amountInPaise, String currency) {
this.amountInPaise = amountInPaise;
this.currency = Objects.requireNonNull(currency);
}
@Override
public boolean equals(Object o) {
if (this == o) return true; // fast path: same object
if (!(o instanceof Money other)) return false; // also handles null
return amountInPaise == other.amountInPaise && currency.equals(other.currency);
}
@Override
public int hashCode() {
return Objects.hash(amountInPaise, currency); // exactly the same fields as equals
}
}
Guidelines:
equals should feed hashCode.== (use Double.compare for double/float, because of NaN and -0.0), and objects with Objects.equals if they may be null.Objects.hash(...) is convenient. For very hot code, a hand-rolled 31 * h + field avoids creating a varargs array.record Money(long amountInPaise, String currency) {}) generate correct equals, hashCode and toString from their components. That's often the simplest answer for value types (see Records). IDEs and Lombok can generate these methods too.instanceof or getClass()?if (!(o instanceof Money other)) return false; // accepts subclasses
if (o == null || getClass() != o.getClass()) return false; // requires the exact same class
With inheritance, instanceof can break symmetry. If ColoredPoint extends Point adds a colour and compares it, then point.equals(coloredPoint) may be true (Point ignores colour) while coloredPoint.equals(point) is false. Options:
final (or records), so no subclasses exist, and then instanceof is safe and idiomatic.getClass() when subclasses must never equal their parent.ColoredPoint has a Point) over extending a value class to add state.Set<Point> points = new HashSet<>();
Point p = new MutablePoint(1, 2);
points.add(p);
p.setX(5); // hash changes; p sits in the bucket for its OLD hash
points.contains(p); // false — looks in the new bucket
points.remove(p); // fails — the object is now effectively stuck in the set
Objects used as HashMap keys or HashSet elements should be immutable, or at least the fields used in equals/hashCode must never change while the object is in the collection.
For database entities, equality based on all fields is usually wrong: fields change and relationships are lazy-loaded. Equality based on a database-generated ID is tricky too, because a new entity has no ID until it's saved, so its hash would change. Common solutions: a natural business key that never changes (like an ISBN), or a UUID assigned at construction, or equality by ID with a constant hashCode. Whatever you choose, keep it stable across the entity's lifecycle.
== vs equals== on references asks "same object?". On primitives it compares values.equals asks "same value?", as defined by the class.String, Integer and other wrappers override equals. Comparing them with == works sometimes, because of string interning and the Integer cache for −128..127, which makes the resulting bugs intermittent and hard to find. Always use equals for objects.Q: Why must you override hashCode when you override equals?
A: Hash-based collections look up an object's bucket by its hash code first, and only call equals within that bucket. If equal objects have different hash codes, lookups search the wrong bucket, so contains fails and duplicates get inserted.
Q: Can two unequal objects have the same hash code?
A: Yes, that's a collision, and it's allowed. Collections handle it by comparing with equals inside the bucket. Many collisions degrade performance (HashMap falls back to tree bins to limit the damage), so a good hashCode spreads values well.
Q: What happens if hashCode returns a constant, like 42?
A: Correctness is preserved (equal objects share a hash), but every element lands in the same bucket. Operations degrade toward O(n), or O(log n) with HashMap's tree bins. It's legal, but slow.
Q: Why is using a mutable object as a HashMap key dangerous?
A: If a field used in hashCode changes after insertion, the entry stays in the bucket for the old hash, and lookups using the new hash miss it. The entry becomes unreachable, a subtle memory leak and logic bug. Use immutable keys.
Q: How do records implement equals and hashCode?
A: The compiler generates them from all components: equals compares each component (using == for primitives, with correct float/double semantics, and Objects.equals for references), and hashCode combines the components' hash codes. Array components are compared by reference, so prefer List in records.