Part 4.1 — equals() and hashCode() Deep Dive¶
This is one of the most misunderstood topics in Java interviews.
Many developers memorize:
"Override both
equals()andhashCode()."
A senior interviewer will ask:
- Why?
- What breaks if you don't?
- How does
HashMapactually use them?
If you cannot answer those questions, you haven't really understood HashMap.
1. What are equals() and hashCode()?¶
Every Java class inherits these methods from Object.
public class Object {
public boolean equals(Object obj);
public int hashCode();
}
Their purposes are different:
equals()→ Checks logical equalityhashCode()→ Produces an integer used for hashing
2. Default Behavior¶
Example:
class Employee {
int id;
Employee(int id) {
this.id = id;
}
}
Employee e1 = new Employee(1);
Employee e2 = new Employee(1);
System.out.println(e1.equals(e2));
Output:
false
Why?
Because the default implementation compares object references.
Memory:
e1 -----> Employee(id=1)
e2 -----> Employee(id=1)
Different objects.
Different references.
Therefore:
false
3. Reference Equality vs Logical Equality¶
Reference equality:
Employee e1 = new Employee(1);
Employee e2 = e1;
System.out.println(e1 == e2);
Output:
true
Both variables point to the same object.
Logical equality:
Employee e1 = new Employee(1);
Employee e2 = new Employee(1);
Different objects.
Same business meaning.
If we override equals() correctly:
true
4. == vs equals()¶
Interview favorite.
String a = new String("Java");
String b = new String("Java");
a == b
Result:
false
Different objects.
a.equals(b)
Result:
true
Same contents.
| Operator | Compares |
|---|---|
== | References (or primitive values) |
equals() | Logical equality (if overridden) |
5. Overriding equals()¶
Example:
class Employee {
int id;
Employee(int id) {
this.id = id;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null || getClass() != obj.getClass())
return false;
Employee other = (Employee) obj;
return id == other.id;
}
}
Now:
Employee e1 = new Employee(100);
Employee e2 = new Employee(100);
System.out.println(e1.equals(e2));
Output:
true
6. What is hashCode()?¶
Example:
Employee e = new Employee(100);
System.out.println(e.hashCode());
Output:
14567382
The actual value is not important.
What matters is the contract.
7. The hashCode() Contract¶
Rule 1¶
If two objects are equal,
they must have the same hash code.
Correct:
e1.equals(e2)
↓
true
↓
hashCode must match
Rule 2¶
If hash codes differ,
objects are definitely not equal.
hash1 != hash2
↓
equals()
need not be checked
This is a major optimization in HashMap.
Rule 3¶
Same hash code does not imply equality.
Example:
Object A
↓
hash = 12
Object B
↓
hash = 12
Still possible:
equals()
↓
false
This is a collision.
8. Why HashMap Uses hashCode() First¶
Suppose:
map.get(employee);
HashMap performs:
Step 1
hashCode()
↓
Bucket
↓
Step 2
equals()
only within bucket
Without hashing:
Need to compare
↓
every key
↓
O(n)
With hashing:
Jump directly
↓
one bucket
↓
O(1) average
9. Internal Lookup Flow¶
Suppose:
Employee e = new Employee(10);
map.get(e);
Internally:
hashCode()
↓
index = hash & (capacity - 1)
↓
Go to bucket
↓
Compare hash
↓
Compare equals()
↓
Return value
Notice:
equals() is not called for every key.
Only keys inside the selected bucket are compared.
10. What Happens If You Override equals() Only?¶
Very common interview question.
Example:
class Employee {
int id;
@Override
public boolean equals(Object obj) {
return id == ((Employee)obj).id;
}
}
No hashCode().
Now:
Employee e1 = new Employee(1);
Employee e2 = new Employee(1);
map.put(e1, "Alice");
System.out.println(map.get(e2));
Expected:
Alice
Actual:
null
Why?
Different default hash codes.
Different buckets.
HashMap never reaches the bucket containing e1.
11. What If You Override hashCode() Only?¶
Example:
@Override
public int hashCode() {
return id;
}
But leave equals() unchanged.
Then:
Employee e1 = new Employee(1);
Employee e2 = new Employee(1);
Same hash.
Same bucket.
But:
equals()
↓
false
HashMap thinks they're different keys.
Result:
Duplicate logical entries.
12. Correct Implementation¶
class Employee {
int id;
Employee(int id) {
this.id = id;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (!(obj instanceof Employee))
return false;
Employee other = (Employee)obj;
return id == other.id;
}
@Override
public int hashCode() {
return Integer.hashCode(id);
}
}
Now:
map.put(new Employee(1), "Alice");
map.get(new Employee(1));
Works correctly.
13. Mutable Keys (Critical Production Bug)¶
Example:
class Employee {
int id;
}
Employee e = new Employee();
e.id = 100;
map.put(e, "Alice");
Later:
e.id = 200;
Now:
map.get(e);
May return:
null
Why?
The object's hash code changed after insertion.
It now belongs in a different bucket than where it was stored.
Rule: Never mutate fields that participate in equals() or hashCode() while the object is being used as a key.
14. Bad hashCode() Implementation¶
Example:
@Override
public int hashCode() {
return 1;
}
Everything goes into:
Bucket 1
Result:
Bucket 1
↓
Node
↓
Node
↓
Node
↓
Node
Performance:
O(n)
Instead of:
O(1)
15. Good hashCode()¶
Example:
@Override
public int hashCode() {
return Objects.hash(id, name);
}
Produces a much better distribution.
Fewer collisions.
Better performance.
16. Objects.hash()¶
Example:
@Override
public int hashCode() {
return Objects.hash(id, department, salary);
}
Readable.
Correct.
Suitable for most business classes.
Interview note: Objects.hash() creates an array internally, so in performance-critical classes you may prefer a manually computed hash (or IDE-generated implementation). The difference is usually negligible for typical enterprise applications.
17. Java Records¶
Java records automatically generate:
equals()
hashCode()
toString()
Example:
record Employee(int id, String name) {}
This is one reason records make excellent immutable map keys.
18. Common Interview Questions¶
Q1. Why must equal objects have equal hash codes?¶
Because HashMap first locates the bucket using hashCode(). If equal objects produce different hash codes, they end up in different buckets and lookups fail.
Q2. Can unequal objects have the same hash code?¶
Yes.
This is called a hash collision.
Q3. Does the same hash code mean objects are equal?¶
No.
equals() must still be used to determine logical equality.
Q4. Which method is called first?¶
hashCode()
↓
equals()
Always.
Q5. Can a bad hash function affect performance?¶
Yes.
Poor distribution increases collisions and degrades lookup performance.
Q6. Why are immutable keys recommended?¶
Because changing a key's state after insertion can change its hash code or equality, making the entry difficult or impossible to retrieve.
equals() Contract¶
A correct implementation must satisfy:
- Reflexive:
x.equals(x)is alwaystrue. - Symmetric: If
x.equals(y)istrue, theny.equals(x)must also betrue. - Transitive: If
x.equals(y)andy.equals(z)aretrue, thenx.equals(z)must also betrue. - Consistent: Repeated calls give the same result if the objects don't change.
- Non-null:
x.equals(null)must always returnfalse.
Violating these rules can cause subtle bugs not only in HashMap, but also in HashSet, TreeSet (when combined with inconsistent comparison logic), and other collections.
Senior Interview Scenario¶
Question:
Map<Employee, String> map = new HashMap<>();
Employee e1 = new Employee(1);
map.put(e1, "Alice");
Employee e2 = new Employee(1);
System.out.println(map.get(e2));
Follow-up:
It prints
null. Why?
Expected reasoning:
HashMapcomputes the bucket usinghashCode().- If
Employeedoesn't overridehashCode(),e1ande2have different identity-based hash codes. e2is searched in a different bucket.equals()is never invoked againste1.- Therefore,
map.get(e2)returnsnull.
Key Takeaways¶
equals()defines logical equality.hashCode()determines the bucket in hash-based collections.HashMapalways useshashCode()beforeequals().- Equal objects must have equal hash codes.
- Collisions are normal and resolved within a bucket using
equals(). - Never use mutable objects as keys unless the fields affecting equality and hashing remain unchanged.
- Records and immutable classes are excellent choices for
HashMapkeys.
Next: Part 4.2 — HashMap Internal Implementation Deep Dive¶
We'll go beneath the API and study the actual insertion and resize algorithms, including:
- The
putVal()method - Bitwise hash spreading (
h ^ (h >>> 16)) - Resize and rehash mechanics
- Why resize is expensive
- Java 7 vs Java 8 resize differences
- Infinite loop issue in Java 7 under concurrent modification
- Memory layout and bucket redistribution
- Source-code-level interview discussion