equals and hashCode, and the Contract Between Them
equals and hashCode are two methods that only work as a pair. Override one and leave the other, and your object behaves correctly right up until it goes into a HashMap or a HashSet, at which point it starts disappearing.
The contract
Three rules matter in practice:
- If
a.equals(b)is true, thena.hashCode() == b.hashCode()must be true. - The reverse does not have to hold. Two unequal objects may share a hash code, which is a collision, and collections handle it.
- Neither result may change while the object sits in a hash based collection.
Rule 1 is the one people break. Rule 3 is the one that bites hardest.
What actually breaks
A hash collection finds an object in two steps: it uses the hash code to pick a bucket, then uses equals to search that bucket. Both steps have to agree.
public class Point {
final int x, y;
@Override
public boolean equals(Object o) {
return o instanceof Point p && p.x == x && p.y == y;
}
// no hashCode
}
Set<Point> set = new HashSet<>();
set.add(new Point(1, 2));
set.contains(new Point(1, 2)); // false
The two points are equal, but they inherit Object.hashCode, which is based on identity. They land in different buckets, so contains never gets as far as calling equals. The object is in the set and cannot be found.
Writing them
Since Java 7 there is no reason to write either by hand:
public class Point {
private final int x;
private final int y;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point other)) return false;
return x == other.x && y == other.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
}
Objects.hash boxes its arguments into an array, so for a hot path with primitive fields 31 * x + y is measurably cheaper. For everything else, readability wins.
Two details in that equals. The this == o shortcut is worth having because self comparison is common inside collections. And instanceof handles null on its own, so an explicit null check is redundant.
instanceof or getClass
instanceof accepts subclasses, getClass() requires the exact type. The difference shows up when a subclass adds state: with instanceof, a Point can equal a ColouredPoint, and equality stops being symmetric because the subclass compares colour and the parent does not.
There is no clean answer, which is why the practical advice is to make classes with value semantics final. Then the question does not arise.
Records do this for you
public record Point(int x, int y) {}
The compiler generates both methods over all components, and they stay correct when you add a component later. This is one of the strongest reasons to reach for records whenever a class is a data carrier, because a hand written equals that someone forgot to update after adding a field is a genuinely hard bug to find.
Mutability, and rule 3
Set<List<String>> set = new HashSet<>();
List<String> list = new ArrayList<>(List.of("a"));
set.add(list);
list.add("b");
set.contains(list); // false
Nothing here is buggy. ArrayList derives its hash from its contents, so mutating it moves it to a different bucket, and the set has no way to know. The object is still in there, taking up space, unreachable by any lookup.
Base equality on fields that do not change. If they all change, the object probably should not be a key.
JPA entities
Entities break all of this in a specific way. Consider the obvious version:
@Entity
public class Order {
@Id @GeneratedValue Long id;
@Override public boolean equals(Object o) {
return o instanceof Order other && Objects.equals(id, other.id);
}
@Override public int hashCode() { return Objects.hash(id); }
}
A new entity has a null id. Put it in a Set, persist it, and the database assigns an id, so the hash code changes while it sits in the set. Rule 3, broken by the framework rather than by you. Worse, two different unsaved entities both have a null id and therefore compare equal.
The workable pattern is a constant hash code plus an id based equals:
@Override
public int hashCode() {
return getClass().hashCode();
}
Every instance of the type collides, which is fine because you rarely hold thousands of one entity type in a single set, and it is stable across the transition from unsaved to saved.
The better answer, when you control the schema, is a business key assigned in the constructor: an order number, a UUID, something the application generates rather than the database. Then the value never changes, the contract holds, and equality means what you actually want it to mean.