Introduction
The equals hashcode contract java was defined in the Object class, which has been part of the JDK since Java 1.0 (May 1996). The contract itself is specified in JLS section 4.3.2 (Object equality) and the contract for hashCode is in the javadoc of java.lang.Object, which says "if two objects are equal according to the equals method, then calling hashCode on each of the two objects must produce the same integer result." Joshua Bloch's Effective Java (2001, Item 9 in the 3rd edition) gives the canonical hash recipe: result = 31 * result + (field == null ? 0 : field.hashCode()), which the JDK's Objects.hash() method (Java 7, July 2011) implements for you.
For a Java learner, the equals hashcode contract java is the difference between a HashMap that returns your value when you look it up and a HashMap that returns null even though the key is "in" the map. HashMap uses hashCode() to find the bucket and equals() to find the entry within the bucket, so if two equal keys have different hash codes the map will look in the wrong bucket and never find the entry. This is one of the most common interview questions and one of the most common production bugs, and the fix is always the same: override both methods together, or use a record (Java 16, March 2021) which does it for you.
What is the equals hashCode Contract in Java?
The equals hashcode contract in Java is the rule that ties the equals() method and the hashCode() method together: if two objects are equal according to equals(), they must have the same hashCode. The contract is one-way -- unequal objects are permitted to have the same hashCode (called a hash collision) -- but the equal-implies-same-hash direction is mandatory. The contract is enforced by the design of HashMap, HashSet, and Hashtable, all of which use hashCode for the bucket and equals for the entry match.
The equals() method itself has a four-part contract specified in JLS 4.3.2: reflexive (x.equals(x) is true), symmetric (x.equals(y) implies y.equals(x)), transitive (x.equals(y) and y.equals(z) implies x.equals(z)), and consistent (multiple calls return the same result as long as neither object changes). Plus, x.equals(null) must return false. The hashCode contract is just two parts: consistent (multiple calls return the same result as long as the object's equals-relevant fields do not change) and equal-implies-same-hash (already mentioned). Violating either contract produces silent, intermittent bugs in HashMap lookups.
How the equals hashCode Contract Works - Step by Step
Step 1: Hash the key. When you call map.put(key, value), HashMap first calls key.hashCode() to get a 32-bit int, then applies a spreading function ((h ^ (h >>> 16)) & (capacity - 1)) to map it to a bucket index. The spreading function ensures that high bits of the hash code are mixed into the low bits, which matters because the capacity is a power of two so only the low bits of the raw hash are used for the index. A bad hashCode that returns the same value for many distinct objects would force them all into the same bucket, degrading the map to a linked list.
Step 2: Find the bucket. Once the index is computed, HashMap looks at the linked list (or tree, since Java 8) at that bucket. If the bucket is empty, the entry is inserted directly. If the bucket has one or more entries, HashMap walks the list and calls key.equals(existing.key) on each. If equals returns true, the existing entry's value is overwritten; if equals returns false for all entries, a new entry is appended.
Step 3: Treeify on long buckets. In Java 8 (March 2014), HashMap was retrofitted to convert a bucket's linked list into a red-black tree when the bucket size exceeds 8 (and shrinks back to a linked list when it drops below 6). The tree is ordered by compareTo if the keys implement Comparable, otherwise by System.identityHashCode. This means a malicious or unlucky key class whose hashCode produces many collisions degrades from O(1) to O(log n) per operation, not O(n) as in older Java versions.
Step 4: Get and remove. map.get(key) does the same hash-and-equal sequence: hash the key, find the bucket, walk the list (or tree), call equals on each existing key. map.remove(key) is the same but unlinks the matched entry. The lookup succeeds if and only if (a) the hashCode of the lookup key matches the hashCode of the stored key and (b) the equals method returns true. If either fails -- because the contract was violated -- the lookup returns null even though the entry is "in" the map.
Step 5: The contract violation. If you override equals but not hashCode, your class inherits Object's hashCode which is the identity hash (memory address). Two objects that are equal per equals will then have different hash codes, so HashMap will look in different buckets for them and never find the second one when you look it up. If you override hashCode but not equals, the bucket may be correct but the entry match will fail because Object's equals is identity. Either way the map silently returns null. The fix is to always override both, together, based on the same set of fields.
Java Program for the equals hashCode Contract
import java.util.*;
public class EqualsHashCodeDemo {
// CORRECT implementation: overrides both equals and hashCode
static class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = 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 && Objects.equals(name, p.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return name + "(" + age + ")";
}
}
// BROKEN: overrides equals but NOT hashCode
static class BrokenPerson {
private final String name;
private final int age;
public BrokenPerson(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof BrokenPerson)) return false;
BrokenPerson p = (BrokenPerson) o;
return age == p.age && Objects.equals(name, p.name);
}
// hashCode inherited from Object: identity hash
}
public static void main(String[] args) {
Person alice1 = new Person("Alice", 30);
Person alice2 = new Person("Alice", 30);
Person bob = new Person("Bob", 25);
System.out.println("=== Person (correct) ===");
System.out.println("alice1.equals(alice2): " + alice1.equals(alice2));
System.out.println("alice1.equals(bob): " + alice1.equals(bob));
System.out.println("alice1.equals(null): " + alice1.equals(null));
System.out.println("alice1.hashCode(): " + alice1.hashCode());
System.out.println("alice2.hashCode(): " + alice2.hashCode());
System.out.println("Hash codes match: "
+ (alice1.hashCode() == alice2.hashCode()));
Map<Person, String> map = new HashMap<>();
map.put(alice1, "Engineer");
map.put(alice2, "Senior Engineer");
map.put(bob, "Manager");
System.out.println("Map size: " + map.size()
+ " (alice1 overwritten by alice2)");
System.out.println("map.get(alice2) = " + map.get(alice2));
System.out.println("map.get(new Person("Alice", 30)) = "
+ map.get(new Person("Alice", 30)));
Set<Person> set = new HashSet<>();
set.add(alice1);
set.add(alice2);
set.add(bob);
System.out.println("Set size: " + set.size());
System.out.println("
=== BrokenPerson (no hashCode override) ===");
BrokenPerson ba1 = new BrokenPerson("Alice", 30);
BrokenPerson ba2 = new BrokenPerson("Alice", 30);
System.out.println("ba1.equals(ba2): " + ba1.equals(ba2));
System.out.println("ba1.hashCode(): " + ba1.hashCode());
System.out.println("ba2.hashCode(): " + ba2.hashCode());
System.out.println("Hash codes match: "
+ (ba1.hashCode() == ba2.hashCode()));
Map<BrokenPerson, String> bmap = new HashMap<>();
bmap.put(ba1, "Engineer");
System.out.println("bmap.get(ba2) = " + bmap.get(ba2));
// null! because ba2 hashes to a different bucket
System.out.println("Contract violated: lookup fails despite equals().");
}
}
Code Explanation - Line by Line
The Person class is the correct implementation. It overrides both equals and hashCode based on the same two fields (name and age). The equals method first checks reference equality (a fast short-circuit), then checks the type with instanceof (which also handles null safely -- null instanceof X is false), then casts and compares each field. The hashCode method delegates to Objects.hash(name, age) which implements Bloch's 31-multiplier recipe: 31 * (31 + nameHash) + ageHash.
The BrokenPerson class is identical except it does NOT override hashCode. It inherits Object's hashCode which returns the identity hash (the memory address of the object). So two BrokenPerson objects that are equal per equals() will have different hash codes, and HashMap will look in different buckets for them and never find the second one when you look it up.
The first test block verifies the equals contract on Person: reflexive (alice1.equals(alice1) is implicit via this==o), symmetric (alice1.equals(alice2) is true and so is alice2.equals(alice1)), and consistent (every call returns the same result). It also verifies that equals returns false for null and for a different person (bob).
The HashMap test is the key demonstration. We put alice1 with value "Engineer", then alice2 with value "Senior Engineer". Because alice1.equals(alice2) is true and their hashCodes match, HashMap finds alice1's bucket, walks the list, calls equals on alice1, gets true, and overwrites the value. The map ends with size 2 (alice2's entry replaced alice1's, bob is separate). Then map.get(new Person("Alice", 30)) constructs a fresh Person object on the heap, but because its hashCode matches and equals returns true against the stored alice2, HashMap finds the entry and returns "Senior Engineer".
The HashSet test is the same pattern: set.add(alice1) inserts; set.add(alice2) sees alice1 is already there (via equals) and skips; set.add(bob) inserts. The set ends with size 2.
The BrokenPerson test shows the contract violation in action. ba1.equals(ba2) returns true (because equals is overridden), but ba1.hashCode() differs from ba2.hashCode() (because hashCode is NOT overridden). When we put ba1 into the map and then look up ba2, HashMap hashes ba2 to a different bucket than ba1 was stored in, finds no entry, and returns null. The map silently "loses" the entry even though ba2 is equal to ba1.
Interview Questions and Answers
Q1. What is the equals hashCode contract in Java?
The contract says that if two objects are equal according to the equals() method, they must produce the same hashCode() result. The reverse is not required: unequal objects may have the same hashCode (a hash collision). The contract is enforced by HashMap, HashSet, and Hashtable, all of which use hashCode for the bucket and equals for the entry match. Violating the contract causes these collections to silently lose entries.
Q2. What happens if you override equals but not hashCode?
Your class inherits Object's hashCode, which returns the identity hash (memory address). Two objects that are equal per your equals method will then have different hash codes, and HashMap will look in different buckets for them. So map.put(key1, value) followed by map.get(key2) (where key1.equals(key2) is true) will return null, because key2 hashes to a different bucket than key1 was stored in. This is a silent bug -- no exception, no warning, just wrong results.
Q3. What are the four properties of the equals contract?
Reflexive: x.equals(x) must return true. Symmetric: if x.equals(y) returns true, then y.equals(x) must also return true. Transitive: if x.equals(y) and y.equals(z) both return true, then x.equals(z) must return true. Consistent: multiple invocations of x.equals(y) must return the same result as long as neither object is modified. Plus, x.equals(null) must return false. Violating any of these breaks the contract and can cause subtle bugs in collections.
Q4. Why does Objects.hash() use the multiplier 31?
Joshua Bloch's Effective Java recommends 31 because it is an odd prime (so multiplying by it does not to zero out high bits when the field hash is small) and because it can be implemented as (i << 5) - i which the JIT can fold into a single shift-and-subtract instruction. Using a prime also reduces collisions when fields are correlated. The choice of 31 over other primes is largely historical and the impact on distribution is small, but it has been the de facto standard since the JDK was first published.
Q5. How does HashMap use equals and hashCode internally?
HashMap calls key.hashCode() to get a 32-bit int, applies a spreading function (h ^ (h >>> 16)) to mix high bits into low bits, masks with (capacity - 1) to get the bucket index, then walks the linked list (or red-black tree since Java 8) at that bucket calling key.equals(existing.key) on each entry. If equals returns true, the existing value is overwritten (on put) or returned (on get). If equals returns false for all entries, the operation either inserts (put) or returns null (get). The hashCode is used once per operation to find the bucket; equals is called per entry in the bucket.
Q6. Can a class have two instances that are equal per equals but produce different hashCodes?
Technically yes (the compiler does not enforce the contract), but it is a contract violation and will break HashMap, HashSet, and Hashtable. The two instances will be stored in different buckets, so a lookup of the second via the first will return null. The fix is to override hashCode based on the same fields as equals. In modern Java, you can use a record (Java 16+) which auto-generates both equals and hashCode from the record components -- this is the simplest way to avoid the bug entirely.
Q7. You have a class with a long id field and you want to use it as a HashMap key. What is the best hashCode implementation?
Use Long.hashCode(id) which is (int)(id ^ (id >>> 32)), mixing the high 32 bits into the low 32 bits so that two longs that differ only in the high bits get different hash codes. Then Objects.hash(id) would also work but adds a boxing allocation for the Long. For a multi-field key (e.g., long id and String name), use Objects.hash(id, name) which calls Long.hashCode on the boxed Long internally. For best performance, hand-roll the recipe as int result = Long.hashCode(id); result = 31 * result + (name == null ? 0 : name.hashCode()); return result; -- this avoids the Object[] allocation that Objects.hash creates.
Sample Input and Output
Input: Run EqualsHashCodeDemo on JDK 11.
Output:
=== Person (correct) ===
alice1.equals(alice2): true
alice1.equals(bob): false
alice1.equals(null): false
alice1.hashCode(): 2332976
alice2.hashCode(): 2332976
Hash codes match: true
Map size: 2 (alice1 overwritten by alice2)
map.get(alice2) = Senior Engineer
map.get(new Person("Alice", 30)) = Senior Engineer
Set size: 2
=== BrokenPerson (no hashCode override) ===
ba1.equals(ba2): true
ba1.hashCode(): 2504210
ba2.hashCode(): 1937118110
Hash codes match: false
bmap.get(ba2) = null
Contract violated: lookup fails despite equals().
The output shows that the correct Person implementation produces matching hash codes (2332976) for two equal instances, so HashMap finds the entry via a fresh-equal key and returns "Senior Engineer". The broken BrokenPerson implementation produces different hash codes (2504210 versus 1937118110) for two equal instances, so HashMap looks in a different bucket and returns null. This is the equals hashCode contract in action -- and violation.
Common Mistakes to Avoid
First, do not override equals without overriding hashCode. The contract violation is silent and intermittent: your code will work in unit tests where you use the same object instance, then fail in production where you use a fresh-equal instance. Second, do not include mutable non-final fields in equals and hashCode. If you put an entry into a HashMap and then mutate a field used in hashCode, the entry becomes stranded in the wrong bucket and lookups will never find it. Mark fields final or compute the hash from immutable fields only.
Third, do not include more fields in equals than in hashCode or vice versa. They must use the same set of fields (the "significant" fields), otherwise two objects that are equal per equals could end up with different hash codes. Fourth, do not use == in equals when comparing fields of reference type (e.g., String); always use Objects.equals() or the field's own equals method. Finally, do not throw NullPointerException from equals when given a null argument; the contract requires x.equals(null) to return false, not throw.
Real-World Applications
The equals hashcode contract java is the foundation of every HashMap, HashSet, ConcurrentHashMap, and LinkedHashMap use in the JDK. Spring's bean definitions are keyed by name (String, with Object's equals), and Spring uses ConcurrentHashMap to store them. JPA entity equality is a famous pain point: entities are usually keyed by their id field, but the id is null until persistence, which forces a custom equals that returns false when id is null. Hibernate's documentation recommends implementing equals based on a business key (e.g., username, email) rather than the database id.
Beyond the JDK, records (Java 16, March 2021) auto-generate equals and hashCode from the record components, eliminating the contract bug entirely for value-like classes. Lombok's @EqualsAndHashCode annotation does the same for non-record classes. Apache Commons Lang's EqualsBuilder and HashCodeBuilder (used widely in pre-Java-7 code) provide builders that follow Bloch's recipe. Anywhere you see a class used as a HashMap key or HashSet element, you are relying on the equals hashCode contract.
Conclusion
The equals hashcode contract java is the rule that ties the equals() and hashCode() methods together so that HashMap, HashSet, and friends work correctly. Override both methods together based on the same set of fields, prefer final fields to avoid stranded entries, and consider records (Java 16+) which auto-generate both methods from the components. Next, study Joshua Bloch's Effective Java Item 10 (toString) and Item 11 (hashCode), the HashMap source code, and the Java records specification (JEP 395).
Comments
Comments
Post a Comment