Memra

The equals() & hashCode() contract

Why you must override both, the contract clauses, and the consequence of getting it wrong in a hash container.

Why hash containers need both methods

HashMap, HashSet, and friends find an element in three steps: (1) call hashCode() to pick a bucket, (2) within that bucket compare candidates with equals(). If you override only one of the two methods, those steps disagree and the container loses your objects.

The default Object.equals is identity (==, same reference) and the default Object.hashCode is derived from the object's identity. So two logically equal objects you created separately are, by default, unequal and have different hash codes. For a value class (a class whose identity is its contents — a Point, a Money, a SkillCode) you almost always want logical equality, which means overriding both.

The contract (you must obey all of it)

equals must be reflexive (x.equals(x)), symmetric (x.equals(y) ⇔ y.equals(x)), transitive, consistent (same result on repeated calls), and x.equals(null) must be false.

The binding rule linking the two methods:

  • If a.equals(b) is true, then a.hashCode() == b.hashCode() MUST be true.
  • The converse is not required: two unequal objects may share a hash code (a collision) — that is allowed and merely costs a little speed.

So: equal objects must have equal hash codes. Violate this and a HashSet may store two objects you consider equal, or fail to find an object you just inserted.

Worked example. A value class Pair overriding both, using the same fields in each, with Objects.equals (null-safe) and Objects.hash:

class Pair {
    final String key; final int value;
    Pair(String key, int value) { this.key = key; this.value = value; }

    @Override public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Pair)) return false;
        Pair p = (Pair) o;
        return value == p.value && Objects.equals(key, p.key);
    }

    @Override public int hashCode() {
        return Objects.hash(key, value);
    }
}

Now new Pair("a", 1).equals(new Pair("a", 1)) is true and both produce the same hashCode(), so a HashSet<Pair> correctly treats them as one element. Note the same fields (key, value) drive both methods — that is what keeps them consistent.

0123equals + hashCode("a",1)one elementequals only("a",1)("a",1)the set keeps bothHashSet<Pair> buckets after adding two equal Pairs.
The container picks a bucket from <code>hashCode()</code> <em>before</em> it ever calls <code>equals()</code>. Computed from the same <code>key</code> and <code>value</code>, the two Pairs land in one bucket, <code>equals</code> matches, and the set holds a single element. Override <code>equals</code> alone and the inherited identity hash scatters them into different buckets where <code>equals</code> is never consulted — so the set stores both, and <code>contains(new Pair("a", 1))</code> answers false for something you just added.
~/memra/learn/comp-308/equals-and-hashcode-contract