Quick answer
In Java, equals decides whether two objects should be considered logically equal. hashCode returns an integer used by hash-based collections such as HashMap and HashSet to find the right bucket.
If two objects are equal according to equals, they must return the same hashCode. If that contract is broken, maps and sets can lose entries, fail lookups, or store duplicates that should not exist.
Override equals and hashCode together, base them on stable identity fields, and avoid mutating fields that participate in equality after the object is used as a key.
Why equals and hashCode matter
Java objects inherit equals and hashCode from Object. The default behavior is identity-based: two references are equal only if they point to the exact same object.
That is not enough for many domain objects.
UserId a = new UserId("u_123");
UserId b = new UserId("u_123");
Most backend code would expect these two values to represent the same user id. Without a custom equals, they are different objects.
This becomes visible when the object is used inside a collection:
Set<UserId> ids = new HashSet<>();
ids.add(new UserId("u_123"));
boolean exists = ids.contains(new UserId("u_123"));
If equals and hashCode are not implemented correctly, exists may be false even though the logical id is already in the set.
The equals contract
The equals method should follow five rules.
| Rule | Meaning |
|---|---|
| Reflexive | x.equals(x) is true. |
| Symmetric | If x.equals(y) is true, y.equals(x) is true. |
| Transitive | If x.equals(y) and y.equals(z), then x.equals(z). |
| Consistent | Repeated calls return the same result while relevant fields do not change. |
| Null-safe | x.equals(null) is false. |
Most bugs come from breaking symmetry, using mutable fields, or comparing the wrong identity.
The hashCode contract
The hashCode contract is shorter but just as important:
- If two objects are equal according to
equals, they must have the same hash code. - If two objects are not equal, they may still have the same hash code.
- The hash code should stay consistent while the object is used in a hash-based collection.
The second point matters: hash collisions are allowed. A good hash function reduces collisions, but Java collections still use equals to confirm the final match.
Read HashMap Collision Handling Explained for the deeper hash table behavior.
A simple value object example
A small value object can implement both methods like this:
import java.util.Objects;
public final class UserId {
private final String value;
public UserId(String value) {
this.value = Objects.requireNonNull(value);
}
public String value() {
return value;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof UserId userId)) {
return false;
}
return value.equals(userId.value);
}
@Override
public int hashCode() {
return Objects.hash(value);
}
}
The object is final, the field is final, and equality is based on one stable value. That makes it safe to use as a key in a map.
Records make this easier
Java records generate equals, hashCode, and toString from record components.
public record UserId(String value) {}
For many simple data carriers and value objects, records are the best choice because they reduce boilerplate and make equality behavior obvious.
Records are not magic, though. If a record component is mutable, equality can still become unstable. Prefer immutable components for objects used as map keys or set members.
HashMap key example
Consider a lookup from user id to permissions:
Map<UserId, Set<String>> permissionsByUser = new HashMap<>();
permissionsByUser.put(new UserId("u_123"), Set.of("read"));
Set<String> permissions = permissionsByUser.get(new UserId("u_123"));
For this lookup to work, the second UserId must hash to the same bucket and compare equal to the stored key.
That requires both methods:
same logical id -> same hashCode -> same bucket -> equals confirms match
If only equals is overridden and hashCode is not, the lookup can fail because the objects may land in different buckets.
The mutable key trap
Do not mutate fields used by equals or hashCode while the object is inside a hash-based collection.
UserKey key = new UserKey("u_123");
Map<UserKey, String> map = new HashMap<>();
map.put(key, "active");
key.setValue("u_999");
String value = map.get(key); // may fail
The entry was stored according to the old hash code. After mutation, the map looks in a different place.
This is one of the most painful collection bugs because the object still exists inside the map, but normal lookup no longer finds it.
equals and inheritance
Equality with inheritance is tricky.
Suppose Money compares amount and currency, while DiscountedMoney adds a discount field. It is easy to create symmetry or transitivity bugs if parent and child classes disagree about what equality means.
For many domain value objects, prefer one of these designs:
- Make the class
final. - Use a record.
- Use composition instead of equality across a class hierarchy.
- Be very deliberate if equality must work across subclasses.
Most backend code does not need clever inheritance-based equality. It needs boring, stable value semantics.
Common mistakes
The first mistake is overriding equals but not hashCode. This breaks HashMap, HashSet, and other hash-based collections.
The second mistake is using mutable fields in equality. If the value changes after insertion, lookup behavior becomes unreliable.
The third mistake is comparing database ids before they exist. A new entity may have a null id until it is saved, so entity equality needs careful design.
The fourth mistake is including too many fields. Equality should represent identity, not every display property.
The fifth mistake is ignoring tests. Equality bugs are cheap to test and expensive to debug later.
Practical testing checklist
Test equality behavior with cases like these:
UserId a = new UserId("u_123");
UserId b = new UserId("u_123");
UserId c = new UserId("u_999");
assertEquals(a, b);
assertEquals(a.hashCode(), b.hashCode());
assertNotEquals(a, c);
assertNotEquals(a, null);
Also test collection behavior:
Set<UserId> ids = new HashSet<>();
ids.add(a);
assertTrue(ids.contains(b));
That last test catches many real mistakes because it verifies the object works in the place where equality matters most.
Related reading
Read HashMap Collision Handling Explained to see how hash codes, buckets, and equality work together. Read HashMap Load Factor Explained for resizing and collision pressure, and HashMap vs Hashtable in Java for modern map choices. Use Time Complexity of Java Collections and the Big-O Cheat Sheet when comparing lookup behavior. For the full sequence, browse the Java Collections Learning Path or Topics.