What is the equals() and hashCode() Contract?
The equals() method must behave as the mathematical notion of 'equality':
🟢 Junior Level
The equals() and hashCode() contract is a set of rules that equals() and hashCode() methods in Java must follow. If violated, collections like HashMap and HashSet will malfunction.
Two simple rules:
- If two objects are equal by
equals(), theirhashCode()must be the same - If
hashCode()is the same — objects are not necessarily equal (this is called a collision)
Correct contract example:
public class Person {
private String name;
private int age;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person p = (Person) o;
return age == p.age && name.equals(p.name);
}
@Override
public int hashCode() {
return 31 * name.hashCode() + age;
}
}
Main rule: if an object will be used as a key in HashMap/HashSet — override both methods. Exception: if you INTENTIONALLY want identity behavior (e.g., for weak references).
🟡 Middle Level
The equals() Contract
The equals() method must behave as the mathematical notion of ‘equality’:
- Reflexivity: an object is equal to itself:
a.equals(a) = true - Symmetry: if A equals B, then B equals A
- Transitivity: if A=B and B=C, then A=C
| Property | Description |
|---|---|
| Reflexivity | x.equals(x) is always true |
| Symmetry | If x.equals(y), then y.equals(x) |
| Transitivity | If x.equals(y) and y.equals(z), then x.equals(z) |
| Consistency | Repeated calls give the same result |
| Null comparison | x.equals(null) is always false |
The hashCode() Contract
- Consistency: value doesn’t change if fields from
equals()haven’t changed - Mandatory link:
x.equals(y)→x.hashCode() == y.hashCode() - Non-mandatory link: same hashCode ≠ equal objects
How to Implement Correctly
Joshua Bloch’s algorithm:
int result = 17;
result = 31 * result + field1;
result = 31 * result + (field2 != null ? field2.hashCode() : 0);
return result;
Why 17 and 31: 17 — initial ‘prime seed’, so the hash of first fields isn’t zero. 31 — an odd prime: multiplication by 31 = left shift by 5 minus 1 (31 * i = (i << 5) - i), which is efficient on CPU. Integer overflow is not a bug — it’s normal: hash ALWAYS wrap-around, this is built into the math.
Common Mistakes
- Overriding equals, forgetting hashCode — duplicates in HashSet
- Using mutable fields — key “breaks” after modification
- Classic JDK bug:
TimestampextendsDate.date.equals(timestamp)may return true, buttimestamp.equals(date)— false, because Timestamp checks nanoseconds that Date doesn’t have. Symmetry is violated!
When NOT to Use Custom equals/hashCode
Don’t use equals/hashCode for objects with business identity that can change. For example, an entity with auto-generated ID: before saving id=null, after — id=42. Two objects with id=null are ‘equal’, but after saving — they’re not. This violates consistency in HashMap.
🔴 Senior Level
Internal Implementation in HashMap
HashMap uses both methods for O(1) search:
hashCode()→ finds bucket:index = (n-1) & hash(key)equals()→ finds key inside the bucket
Consequences of violation:
- Equal objects with different hashCodes land in different buckets →
get()returnsnull HashSetallows duplicates → business logic violationremove()doesn’t find the object → memory leak
Symmetry Violation in Inheritance
class Point2D { int x, y; }
class Point3D extends Point2D { int z; }
If Point3D.equals() checks all 3 fields, but Point2D.equals() only 2:
new Point2D(1,1).equals(new Point3D(1,1,0))→true(Point2D doesn’t see z)new Point3D(1,1,0).equals(new Point2D(1,1))→false(Point3D not instanceof Point3D)- Symmetry violation!
Solution: use getClass() instead of instanceof, or composition instead of inheritance.
Edge Cases
- URL.equals() — makes DNS requests, violates consistency on network failure
- Float.NaN —
Float.NaN.equals(Float.NaN)= true, butFloat.NaN == Float.NaN= false - Arrays —
equals()compares references, not content; useArrays.equals()
Performance Implications
- Poor hashCode → collisions → HashMap degradation to O(log n) or O(n)
- Heavy equals() (comparing large strings/byte arrays) → large constant in O(1)
- Caching hashCode (as in
String) — critical for performance
Java 14+: Records
public record Person(String name, int age) {}
Records automatically generate equals() and hashCode() based on all components, guaranteeing contract compliance.
Security
Contract violations can lead to DoS attacks via Hash Flooding. A quality hashCode() is not just correctness, but also security.
🎯 Interview Cheat Sheet
Must know:
- equals() — reflexivity, symmetry, transitivity, consistency, null = false
- hashCode() — if equals = true, hashCode MUST match
- The reverse is false: same hashCode ≠ equal objects (collision)
- Bloch’s algorithm: initial 17, multiplier 31 (=(i«5)-i), include all fields from equals
- 31 — odd prime, JVM optimizes multiplication via shift
- Integer overflow — normal, hash always wrap-around
- Records (Java 14+) automatically generate both methods
Common follow-up questions:
- What happens if you override only equals? — equal objects land in different buckets, get returns null
- Why 31, not 17 or 37? — 31 = (i«5)-i, one operation instead of multiplication
- What is symmetry violation? — A.equals(B) = true, but B.equals(A) = false (classic bug: Timestamp vs Date)
- When NOT to override? — when identity semantics is needed (comparison by reference, not content)
Red flags (DO NOT say):
- “hashCode must be unique” — no, collisions are allowed
- “You can override only one method” — no, always the pair
- “URL is a good key” — no, URL.equals() makes DNS requests, violates consistency
Related topics:
- [[03. How HashMap Determines Which Bucket to Put an Element In]]
- [[08. If Two Objects Are Equal by equals(), What Can You Say About Their hashCode()]]
- [[09. If Two Objects Have the Same hashCode(), Are They Necessarily Equal by equals()]]
- [[10. What Happens If You Override equals() But Not hashCode()]]