“What is the contract between equals and hashCode, and what happens if you override only one?” is probably the most frequently asked Java interview question of all. It is asked at every level because the answer reveals whether a candidate understands how hash-based collections actually work, not just that they exist.
A strong answer states the contract precisely, explains how HashMap relies on it, shows a correct implementation, and mentions the subtle failure that catches experienced developers: mutating a key after it has been stored.
Before you start
You should know Java classes and fields, the difference between == (same reference) and equals (same logical value), and how to use HashMap and HashSet. Remember that every class inherits equals and hashCode from Object, where equals means identity and hashCode is typically derived from the object’s identity, not its fields.
The short answer
If a.equals(b) is true, then a.hashCode() must equal b.hashCode(). The reverse is not required: unequal objects may share a hash code, which is just a collision. HashMap and HashSet use hashCode to pick a bucket and then equals to find the matching key within it, so overriding equals without hashCode makes two equal keys land in different buckets and lookups fail. Override both, base them on the same immutable fields, and do not change those fields while the object is in a hashed collection.
How it works
The Object.equals Javadoc requires equals to be:
- reflexive:
x.equals(x)is true, - symmetric:
x.equals(y)equalsy.equals(x), - transitive: if
x.equals(y)andy.equals(z)thenx.equals(z), - consistent: repeated calls return the same answer while the fields used are unchanged,
- and
x.equals(null)is false.
hashCode must return the same value on repeated calls while the fields are unchanged, and equal objects must have equal hash codes.
Here is why both matter. A simplified HashMap.get:
V get(Object key) {
int h = key.hashCode();
Node<K, V> node = table[(table.length - 1) & spread(h)]; // pick a bucket
while (node != null) {
if (node.hash == h && (node.key == key || key.equals(node.key))) {
return node.value; // equals confirms the match
}
node = node.next;
}
return null;
}If two equal keys have different hash codes, get searches the wrong bucket and never calls equals on the stored key. If hashCode is correct but equals is not overridden, the right bucket is found but identity comparison rejects the separately created key.
class CustomerKey {
private final String region;
private final long number;
CustomerKey(String region, long number) { this.region = region; this.number = number; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof CustomerKey other)) return false;
return number == other.number && region.equals(other.region);
}
// hashCode NOT overridden
}
Map<CustomerKey, String> map = new HashMap<>();
map.put(new CustomerKey("EU", 42), "Asha");
System.out.println(map.get(new CustomerKey("EU", 42))); // almost always nullStep-by-step walkthrough
Step 1: Choose the identifying fields
Decide what makes two instances “the same”. For a customer key, region and number. Leave out caches, timestamps and derived values. Every field used in equals should also feed hashCode, and no field outside equals should feed hashCode.
Step 2: Write equals carefully
@Override
public boolean equals(Object o) {
if (this == o) return true; // fast path
if (!(o instanceof CustomerKey other)) return false; // also handles null
return number == other.number
&& Objects.equals(region, other.region); // null-safe
}The parameter type must be Object. Writing equals(CustomerKey other) overloads instead of overriding, and collections keep calling the inherited identity version. The @Override annotation turns that mistake into a compile error. Use instanceof for final classes; for class hierarchies, getClass() != o.getClass() keeps symmetry when subclasses add fields.
Step 3: Derive hashCode from the same fields
@Override
public int hashCode() {
return Objects.hash(region, number);
}Objects.hash multiplies by 31 and adds each field’s hash. It boxes primitives and allocates a varargs array, so hot paths sometimes write 31 * region.hashCode() + Long.hashCode(number) by hand. Either is correct; a constant like return 1; is also technically legal but turns every map into a linked list.
Step 4: Prefer a record for value types
record CustomerKey(String region, long number) {}A record generates equals, hashCode and toString from all components, and its fields are final. For keys and value objects this removes the whole class of bugs.
Worked scenario
An order service caches pending orders in a HashSet<Order>, where Order.equals and hashCode use id and status. After an order is paid, the code updates it in place:
Set<Order> pending = new HashSet<>();
pending.add(order); // stored under hash of (id, PENDING)
order.setStatus(Status.PAID); // hash would now be different
pending.remove(order); // returns false
pending.contains(order); // false, yet iterating the set still shows the orderThe entry still sits in the bucket chosen from the old hash. remove computes the new hash, looks in a different bucket and finds nothing. The set now holds an object it cannot find, which over time behaves like a memory leak.
The fix is to base identity on what never changes:
record OrderId(String value) {}
class Order {
private final OrderId id;
private Status status;
@Override public boolean equals(Object o) {
return o instanceof Order other && id.equals(other.id);
}
@Override public int hashCode() { return id.hashCode(); }
}Better still, key the map by OrderId (Map<OrderId, Order>) so mutable state never participates in hashing at all.
Common mistake
- “Equal hash codes mean equal objects.” No. Collisions are allowed;
"Aa"and"BB"both hash to 2112. - Overloading instead of overriding.
public boolean equals(CustomerKey k)is never called by collections. - Overriding only equals. Lookups in
HashMapandHashSetfail for separately created equal keys. - Mutable fields in hashCode. Changing them after insertion strands the entry in the wrong bucket.
- Breaking symmetry with subclasses. A
ColorPointthat equals aPointbut not vice versa breaks the contract; prefer composition orgetClass()checks.
Verify the behavior
A JUnit test that exercises the contract and the collection behaviour:
import static org.junit.jupiter.api.Assertions.*;
import java.util.*;
import org.junit.jupiter.api.Test;
class CustomerKeyTest {
record CustomerKey(String region, long number) {}
@Test
void equalKeysHaveEqualHashesAndFindEntries() {
var a = new CustomerKey("EU", 42);
var b = new CustomerKey("EU", 42);
assertNotSame(a, b);
assertEquals(a, b);
assertEquals(a.hashCode(), b.hashCode());
Map<CustomerKey, String> map = new HashMap<>();
map.put(a, "Asha");
assertEquals("Asha", map.get(b));
}
@Test
void collisionsAreLegal() {
assertEquals("Aa".hashCode(), "BB".hashCode());
assertNotEquals("Aa", "BB");
}
}For hand-written classes, the EqualsVerifier library checks symmetry, transitivity, nullity and hash consistency automatically in one line.
Follow-up questions
What is the default hashCode? An identity hash that the JVM generates and stores in the object header on first use. It is not the memory address, because objects move during garbage collection.
Why is 31 used in hash functions? It is an odd prime, so multiplication spreads bits well, and 31 * i can be computed as (i << 5) - i. It is convention, not a requirement.
Can two objects be equal under compareTo but not equals? Yes, for example BigDecimal("1.0") and BigDecimal("1.00"). That is why TreeSet and HashSet can disagree about duplicates.
Should equals compare arrays with equals? No. Arrays inherit identity equals; use Arrays.equals and Arrays.hashCode. Records with array components have this exact pitfall.
Interview exercise
A Point class with x and y overrides equals but its hashCode returns x only. Is the contract violated? What is the practical consequence of a million points along the line x = 0?
Answer and reasoning
The contract is not violated. Equal points have equal x, so they produce equal hash codes, which is the only requirement. The consequence is performance: every point with x = 0 lands in the same bucket. Since Java 8, HashMap converts a bucket with more than eight entries into a red-black tree once the table has at least 64 slots, but Point does not implement Comparable, so the tree can only order by hash and identity, and lookups degrade toward linear scans. The fix is Objects.hash(x, y) or 31 * x + y, so both fields contribute. The reasoning shows you know that correctness and quality of a hash are separate questions.
Continue learning
Keep practising with the Java interview questions and the Java MCQs. Go deeper with HashMap collisions and key quality, records and shallow immutability and comparator consistency. The authoritative contract is in the Object API documentation.