Java interview question
Explain the equals() and hashCode() contract. What breaks if you violate it?
Short answer
If two objects are equal by equals(), they must return the same hashCode(); unequal objects may share a hash. Hash-based collections find the bucket by hashCode and then compare with equals, so overriding only equals makes HashSet and HashMap lookups fail. Records and Objects.hash make correct implementations easy.
Answer
The contract, defined in java.lang.Object, says: if a.equals(b) is true, then a.hashCode() == b.hashCode() must also be true. The reverse is not required: unequal objects may share a hash code (a collision), it just makes hash tables slower. Additionally, equals must be reflexive, symmetric, transitive, consistent across calls, and return false for null. If you override equals, you must override hashCode.
Why it matters: how hash collections work
HashMap, HashSet and friends look up an element in two steps:
- Compute
hashCode()to find the bucket. - Within that bucket, compare candidates with
equals().
If two “equal” objects have different hash codes, step 1 sends the lookup to the wrong bucket and equals is never called.
The failure, concretely
class 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: uses Object's identity-based hash
}
Set<User> users = new HashSet<>();
users.add(new User("a@example.com"));
users.contains(new User("a@example.com")); // almost certainly false
users.add(new User("a@example.com"));
users.size(); // usually 2: "duplicates" in a Set
In test code this shows up as deduplication that does not deduplicate, assertTrue(list.contains(expected)) failing even though the objects look identical, and Map lookups of expected results returning null.
A correct implementation
import java.util.Objects;
final class User {
private final String email;
private final String role;
User(String email, String role) { this.email = email; this.role = role; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User other)) return false;
return Objects.equals(email, other.email) && Objects.equals(role, other.role);
}
@Override
public int hashCode() {
return Objects.hash(email, role);
}
}
Use the same fields in both methods. Objects.equals handles nulls.
Easiest in Java 16+: records
record User(String email, String role) {}
new User("a@example.com", "admin").equals(new User("a@example.com", "admin")); // true
Records generate equals, hashCode and toString from their components. They are ideal for test data and expected-result objects. Lombok’s @EqualsAndHashCode or @Value, or IDE generation, are alternatives in older codebases.
Other pitfalls
- Mutable keys: if a field used in
hashCodechanges after the object is put in aHashSet, the object is stranded in the wrong bucket. Keep keys immutable. - Inheritance and symmetry: using
getClass() != o.getClass()versusinstanceofmatters when subclasses add fields.instanceofwith a subclass that adds state can break symmetry, which is one reason to make value classesfinal. - JPA entities: equality based on a generated ID that is null before persisting causes subtle bugs; that is a separate, well-known debate.
What interviewers look for
- The exact direction of the rule: equal implies same hash, not vice versa.
- The bucket-then-equals mechanism.
- A concrete failure with
HashSetorHashMap. - Modern solutions: records,
Objects.hash, and avoiding mutable keys.
A strong answer covers
- Equal objects must have equal hash codes; the reverse is not required
- equals must be reflexive, symmetric, transitive, consistent, and false for null
- Explains how HashMap/HashSet use hashCode then equals
- Shows the failure when only equals is overridden
- Uses records, Objects.equals/Objects.hash, or IDE/Lombok generation; avoids mutable keys