On this page
Description
When are two objects "the same"? By default, both Java and Python say: only if they're the very same object in memory. For values - points, money, dates, ids - you usually want them compared by their contents instead, and hash-based collections (HashMap, HashSet, dict, set) depend on you doing it correctly. This skill covers the equality contract, why equals and hashCode (__eq__ and __hash__) must always change together, and the bugs that follow when they don't.
Here's the kind of bug we mean:
Java
class Point {
final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
}
Set<Point> visited = new HashSet<>();
visited.add(new Point(1, 2));
visited.contains(new Point(1, 2)); // false!
Python
class Point:
def __init__(self, x: int, y: int):
self.x, self.y = x, y
visited = set()
visited.add(Point(1, 2))
Point(1, 2) in visited # False!
Two points with the same coordinates are different objects, and by default objects are compared by identity. The set can't find the point, even though it's "right there".
We'll go from simple to more technical:
- The contract: the rules equality must follow, and the one rule linking it to hashing.
- How hash collections use both methods, step by step.
- Where it breaks: mutable keys, inheritance, and comparisons that disagree.
The contract
To compare by value, override equals - and whenever you override equals, you must override hashCode too (in Python, __eq__ and __hash__ - and Python enforces half of it: a class that defines __eq__ without __hash__ gets __hash__ = None and can't go in a set or be a dict key at all):
- Reflexive:
a.equals(a)is true. - Symmetric:
a.equals(b)if and only ifb.equals(a). - Transitive: if
a.equals(b)andb.equals(c), thena.equals(c). - Consistent: the answer doesn't change unless the objects change.
equals(null)is false.- Equal objects must have equal hash codes. (Unequal objects may share a hash code.)
Java
final class Point {
private final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point p)) return false;
return x == p.x && y == p.y;
}
@Override public int hashCode() { return Objects.hash(x, y); }
}
Python
class Point:
def __init__(self, x: int, y: int):
self._x, self._y = x, y
def __eq__(self, other: object) -> bool:
if not isinstance(other, Point):
return NotImplemented # let Python try the other side, then fall back to identity
return (self._x, self._y) == (other._x, other._y)
def __hash__(self) -> int:
return hash((self._x, self._y))
# Or simply: @dataclass(frozen=True) with fields x and y
Or simply: record Point(int x, int y) {} - records generate a correct equals and hashCode from their components. (Python: @dataclass(frozen=True) generates __eq__ and __hash__ from the fields.)
How hash collections use it, step by step
HashMap.put(key, value)computeskey.hashCode()to choose a bucket.- Inside that bucket it uses
equalsto find a matching key. get(key)repeats the same two steps.- If equal keys had different hash codes, they'd land in different buckets and
getwould miss. - If a key's hash code changes after insertion, it's stranded in the wrong bucket forever.
Where you'll see it
- Every
HashMap,HashSet,ConcurrentHashMapand caching library relies on this contract. String,Integerand records all implement value equality correctly.- ORMs (JPA/Hibernate) are notorious for equality bugs on entities - usually solved by comparing on a stable business key or the id.
Which tool for which job?
| You want to | Java | Python |
|---|---|---|
| A small value type with correct equality and hashing | record | @dataclass(frozen=True) |
| Hand-written value equality | equals + hashCode (Objects.hash) | __eq__ (return NotImplemented for other types) + __hash__ |
| Equality for an entity (a customer, an order) | compare a stable id or business key | the same |
| Identity semantics (a connection, a running job) | keep the default | keep the default |
| A sorted set or map | compareTo consistent with equals | sort keys consistent with == |
Common mistakes
| Mistake | What happens | Do this instead |
|---|---|---|
equals without hashCode | duplicates in sets, lost map entries (Python refuses: the object becomes unhashable) | always override both |
| Changing a field used in the hash while the object is in a set or map | the entry becomes unreachable | immutable keys |
A subclass adding a field to equals | a.equals(b) but not b.equals(a) | final value classes, or composition |
compareTo disagreeing with equals | TreeSet and HashSet disagree about duplicates | keep them consistent |
Hashing a field that equals ignores | equal objects with different hashes | hash only what equals compares |
Check yourself
Try to answer before opening each one - recalling beats re-reading.
1. You override equals but not hashCode. What goes wrong with a HashSet?
Show answer
Equal objects usually get different (identity-based) hash codes, land in different buckets, and the set treats them as different - duplicates appear and contains returns false.
2. Why is it dangerous to modify an object after using it as a HashMap key?
Show answer
If the change affects hashCode, the entry stays in the old bucket; looking it up with the new hash searches the wrong bucket, so it's effectively lost.
3. What's the quickest way to get a correct equals and hashCode for a small value type?
Show answer
Declare it as a record - the compiler generates both from the components.
Key takeaways
- By default,
equalsmeans "same object"; override it for value equality. - Override
equals⇒ overridehashCode: equal objects must have equal hash codes. - Never mutate a field used in
hashCodewhile the object is inside a hash collection - prefer immutable keys. - Keep
compareToconsistent withequals; preferfinalclasses or records for value types.