OOP Fundamentals · Core OOP Concepts

equals, hashCode & Object Contracts

The equals/hashCode contract, why hash-based collections depend on it, and the classic bugs: mutable keys, broken symmetry and inconsistent ordering.

  • 4 min read
  • 8 practice questions
  • 2 graded problems
On this page
  1. Description
  2. The contract
  3. How hash collections use it, step by step
  4. Where you'll see it
  5. Which tool for which job?
  6. Common mistakes
  7. Check yourself
  8. Key takeaways
  9. Quick revise
  10. Practice

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:

  1. The contract: the rules equality must follow, and the one rule linking it to hashing.
  2. How hash collections use both methods, step by step.
  3. 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 if b.equals(a).
  • Transitive: if a.equals(b) and b.equals(c), then a.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

  1. HashMap.put(key, value) computes key.hashCode() to choose a bucket.
  2. Inside that bucket it uses equals to find a matching key.
  3. get(key) repeats the same two steps.
  4. If equal keys had different hash codes, they'd land in different buckets and get would miss.
  5. 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, ConcurrentHashMap and caching library relies on this contract.
  • String, Integer and 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 toJavaPython
A small value type with correct equality and hashingrecord@dataclass(frozen=True)
Hand-written value equalityequals + hashCode (Objects.hash)__eq__ (return NotImplemented for other types) + __hash__
Equality for an entity (a customer, an order)compare a stable id or business keythe same
Identity semantics (a connection, a running job)keep the defaultkeep the default
A sorted set or mapcompareTo consistent with equalssort keys consistent with ==

Common mistakes

MistakeWhat happensDo this instead
equals without hashCodeduplicates 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 mapthe entry becomes unreachableimmutable keys
A subclass adding a field to equalsa.equals(b) but not b.equals(a)final value classes, or composition
compareTo disagreeing with equalsTreeSet and HashSet disagree about duplicateskeep them consistent
Hashing a field that equals ignoresequal objects with different hasheshash 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, equals means "same object"; override it for value equality.
  • Override equals ⇒ override hashCode: equal objects must have equal hash codes.
  • Never mutate a field used in hashCode while the object is inside a hash collection - prefer immutable keys.
  • Keep compareTo consistent with equals; prefer final classes or records for value types.

Quick revise

The whole skill on one page, for the night before an interview.

Everything from the lesson and the deep dive on one page. Skim it before practice or an interview, and open the full lesson for anything that doesn't ring a bell.

Pick the tool

You want toJavaPython
A value typerecord@dataclass(frozen=True)
Hand-written equalityequals + hashCode__eq__ + __hash__
An entitycompare a stable id or business keythe same
Identity semanticsthe defaultthe default

Snippets worth knowing by heart

Java

@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

def __eq__(self, other: object) -> bool:
    if not isinstance(other, Point):
        return NotImplemented
    return (self.x, self.y) == (other.x, other.y)

def __hash__(self) -> int:
    return hash((self.x, self.y))

The traps

  • equals without hashCode - duplicates and lost keys (Python: unhashable).
  • Mutating a hashed field - the entry is stranded.
  • A subclass adding a field to equality - symmetry breaks.
  • compareTo disagreeing with equals - sorted and hashed sets disagree.

Staff-level points, one line each

  • No subclass can add a value field and keep the contract - final value classes or composition.
  • Return NotImplemented, not False, for unknown types in __eq__.
  • A constant hash: 3,000 inserts took 0.22 s vs 0.3 ms; Java treeifies big buckets only for Comparable keys.
  • String, str and (3.14+) tuple cache their hash; Java records and lists recompute it every time.
  • Map<Long, String>.get(1) is null - Integer never equals Long.
  • Python: 1 == 1.0 == True hash alike - {1: "a", True: "b", 1.0: "c"} is {1: 'c'}.
  • Java Double.equals: NaN equals NaN, 0.0 doesn't equal -0.0; Python: two NaN objects are two set elements.
  • Entities hashed on a database id vanish from sets when saved - use a business key or app-assigned ids.
  • Default hashes are identity-based: Java's lives in the object header; CPython's is the address >> 4.

Say it in an interview

  • "It's a value object, so I'd make it a record and get equality and hashing for free."
  • "Keys are immutable - a key that changes is lost in the map."
  • "The entity compares on its order number, which exists before it's saved."
  • "I'd use composition rather than add a field to equality in a subclass."

Practice

This skill comes with 8 multiple-choice questions that tell you which misconception a wrong answer reveals and 2 graded problems. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice equals, hashCode & Object Contracts free

Fix the Version Key That Gets Lost

MediumProCoding problemJava · Python

Version is used as a HashMap key, but it has three bugs that make release notes disappear and sort wrongly:

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Practice equals, hashCode & Object Contracts free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime