Back to Blog
Java

Java equals hashCode Contract: Rules and Implementation

java equals hashcode contract: Learn the Java equals-hashCode contract, why hash-based collections depend on it, and how to implement both methods correctly to avoid s...

equals()hashCode()HashMapJava recordsObject contract
Illustration of two equal Java objects converging into the same hash bucket in a HashMap, representing the equals and hashCode contract.

When a Java class overrides equals() to define logical equality, the java equals hashcode contract requires that hashCode() be overridden as well. The rule itself is short: if two objects are equal according to equals(), they must return the same value from hashCode(). The consequences of ignoring that rule are anything but short — hash-based collections silently misbehave, and the resulting bugs are difficult to trace back to their cause.

The Contract Between equals() and hashCode()

The contract is documented on Object.hashCode() and has three clauses:

  • If x.equals(y) is true, then x.hashCode() == y.hashCode() must be true.
  • If x.equals(y) is false, the two objects may still have equal hash codes. This is allowed and expected.
  • hashCode() must be consistent: repeated calls on the same object return the same value, provided no field used by equals() has changed.

The second clause is the one developers often misunderstand. Unequal objects landing in the same hash bucket is normal hash-table behavior. The contract does not require unique hash codes; it only forbids equal objects from having different ones.

Why Hash-Based Collections Depend on the Contract

HashMap, HashSet, and Hashtable use the hash code to decide which bucket an entry belongs to. During a lookup, the collection computes the key's hash, locates the bucket, and then uses equals() to find the exact match inside that bucket.

When two equal keys produce different hash codes, they land in different buckets. A lookup with a key equal to the one that was stored searches the wrong bucket, finds nothing, and returns null or false.

Map<Person, String> directory = new HashMap<>(); Person alice = new Person("Alice", 30); directory.put(alice, "Engineering"); Person sameAlice = new Person("Alice", 30); String result = directory.get(sameAlice); // If equals() is overridden but hashCode() is not, // result is null even though sameAlice.equals(alice) is true.

The default hashCode() from Object is identity-based, so two distinct instances of the same class virtually never share a hash. The lookup fails every time, not occasionally.

What Happens When the Contract Is Broken

The symptoms vary by collection and operation:

  • HashMap.get() returns null for a key that was stored under an equal key.
  • HashSet.contains() returns false for an element that was added.
  • HashSet silently accepts duplicates, because the second insertion lands in a different bucket and never encounters the first element.
  • remove() fails, leaving stale entries behind.

None of these failures produce an exception. The program keeps running and returns wrong results, which makes the root cause hard to identify. A class that overrides equals() without hashCode() is the most common source of this class of bug.

Implementing equals() Correctly

A correct equals() satisfies five properties:

  • Reflexive: x.equals(x) is true.
  • Symmetric: x.equals(y) is true exactly when 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.
  • Non-null: x.equals(null) is false.

A standard implementation follows a fixed pattern:

@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person person = (Person) o; return age == person.age && Objects.equals(name, person.name); }

The getClass() check prevents the symmetry violation that instanceof introduces. If a subclass adds fields and overrides equals() using instanceof, a parent instance can be equal to a child instance while the child is not equal to the parent.

Implementing hashCode() Correctly

hashCode() must derive its value from the same fields that equals() compares. If equals() uses name and age, then hashCode() must use name and age as well.

The conventional algorithm starts with a small prime and combines each field's hash:

@Override public int hashCode() { int result = 31; result = 31 * result + Integer.hashCode(age); result = 31 * result + (name != null ? name.hashCode() : 0); return result; }

The multiplier 31 is an odd prime that produces a reasonable distribution while remaining cheap to compute. The JVM can optimize 31 * x as (x << 5) - x.

For most classes, Objects.hash() is simpler and equally correct:

@Override public int hashCode() { return Objects.hash(name, age); }

Objects.hash() allocates a varargs array on each call, so a hand-written version is marginally cheaper in hot paths. In ordinary application code, the difference is negligible.

Common Pitfalls That Break the Contract

Mutable fields. When a field used by hashCode() changes after the object is placed in a HashMap, the object's bucket changes. The map still looks in the old bucket, and the entry becomes unreachable.

Person p = new Person("Alice", 30); map.put(p, "Engineering"); p.setAge(31); // hash changes; the entry is now lost in the old bucket

The fix is to make such fields final or to exclude mutable state from equals() and hashCode().

Inheritance and symmetry. Using instanceof in equals() instead of getClass() breaks symmetry when subclasses override equals(). A parent object equals a child object, but the child's equals() returns false for the parent.

Updating one method without the other. Adding a field and updating only equals() — or only hashCode() — silently violates the contract. The two methods must change together.

Performance Considerations for hashCode()

Hash quality directly affects HashMap lookup cost. When hash codes are poorly distributed, many entries collide in the same bucket, and lookup degrades from O(1) toward O(n).

The built-in hash codes of String, Integer, and other standard types are well distributed. For custom classes, the 31-multiplier algorithm is a safe default. A common mistake is returning a constant — for example, return 1;. That is contract-compliant, because all equal objects share the same hash, but it places every entry in one bucket and turns the map into a linked list.

There is no need for a cryptographic hash. The goal is only that different objects tend to produce different values.

Modern Alternatives: Records and Generated Code

Java 16 introduced records, which generate equals(), hashCode(), and toString() from the component list:

public record Person(String name, int age) {}

The generated methods follow the contract exactly and use all components. Records are the right choice when a class is a simple data carrier with no mutable state.

Lombok's @EqualsAndHashCode annotation generates both methods as well. It offers options such as callSuper to include superclass fields. When using Lombok, be explicit about which fields participate, because the default includes all non-static, non-transient fields.

Generated code does not remove the need to understand the contract. The same rules apply when configuring which fields participate: a field in equals() must also appear in hashCode().

Testing the Equals-HashCode Contract

A focused unit test catches violations early:

Person a = new Person("Alice", 30); Person b = new Person("Alice", 30); assertTrue(a.equals(b)); assertEquals(a.hashCode(), b.hashCode());

Thorough tests also cover reflexivity, symmetry, transitivity, consistency, and the null case. Property-based testing that generates random field values and checks a.equals(b) implies a.hashCode() == b.hashCode() is a practical way to verify classes with many fields.

One edge case deserves attention: Float and Double fields. Float.equals() treats Float.NaN as equal to itself, while Float.compare() distinguishes -0.0f from 0.0f. If equals() uses Float.compare() and hashCode() uses Float.hashCode(), the two methods can disagree. The safest approach is to use Float.hashCode() and Double.hashCode() consistently in both methods, or to rely on Objects.equals() for boxed values.

java equals hashcode contract: Practical Usage and Code Exam | RYUSLOG DEV