Java

The equals() and hashCode() Contract

The equals() and hashCode() methods are the foundation of how Java collections work. Get them wrong, and objects "disappear" from HashMaps, duplicates appear in HashSets, and tests fail mysteriously. Get them right, and your collections behave predictably and efficiently.

This is the #1 most-asked topic in Java interviews for good reason - it reveals whether a developer truly understands how collections work under the hood.

πŸ“‹ At a Glance

AspectDetails
Core Methodsequals(), hashCode() from Object class
RelatedComparable.compareTo(), Comparator.compare()
ContractMathematical properties that must hold
ImpactHashSet, HashMap, TreeSet, TreeMap behavior
Key Insightequals() = true ⟹ hashCode() must be same

🎯 What You'll Learn

After reading this article, you will be able to:

  • Explain the contracts for equals(), hashCode(), and Comparable
  • Implement correct equals/hashCode for any class
  • Identify bugs when contracts are violated
  • Understand why mutable keys cause problems in HashMaps
  • Use modern alternatives like Records and Lombok safely
  • Debug issues where objects "disappear" from collections

πŸ”₯ Production Story: The Vanishing Orders

An e-commerce platform had a critical bug: orders were "disappearing" from the cache. Customer service was getting complaints about orders not appearing in the dashboard, even though they existed in the database.

After two days of debugging, the team found the culprit:

JAVA(23 lines)
Code
Loading syntax highlighter...
The cache used a HashSet<Order>. Here's what happened:
JAVA(14 lines)
Code
Loading syntax highlighter...
The fix:
JAVA(18 lines)
Code
Loading syntax highlighter...
The lessons:
  1. Only include immutable fields in equals/hashCode
  2. For entities, identity (ID) is usually the only field needed
  3. Mutable fields in hash calculations = time bombs

🧠 Mental Model: The Library Card Catalog

Think of hashCode() and equals() like a library's card catalog system:
TEXT(26 lines)
Code
Loading syntax highlighter...
Key insight: hashCode() is for speed (narrow down the search), equals() is for correctness (confirm exact match).

πŸ”¬ Deep Dive

1. The equals() Contract

The equals() method must satisfy these mathematical properties:
JAVA(23 lines)
Code
Loading syntax highlighter...
Breaking symmetry (common mistake):
JAVA(30 lines)
Code
Loading syntax highlighter...
The fix - use composition instead of inheritance:
JAVA(22 lines)
Code
Loading syntax highlighter...

2. The hashCode() Contract

JAVA(21 lines)
Code
Loading syntax highlighter...
Why 31 as the multiplier?
JAVA(9 lines)
Code
Loading syntax highlighter...

Why 31?

  • Odd prime: Primes give better distribution than even numbers
  • One less than power of 2: 31 * x = (x << 5) - x (fast bit shift optimization)
  • Good distribution: Empirically produces fewer collisions for string-like data
TEXT(5 lines)
Code
Loading syntax highlighter...

3. The Relationship: equals() and hashCode()

TEXT(23 lines)
Code
Loading syntax highlighter...

4. What Happens When You Break the Contract

JAVA(31 lines)
Code
Loading syntax highlighter...

5. Implementing equals() and hashCode() Correctly

The Standard Pattern:
JAVA(25 lines)
Code
Loading syntax highlighter...
Why getClass() instead of instanceof?
JAVA(8 lines)
Code
Loading syntax highlighter...

Rule of thumb:

  • Use getClass() for concrete classes (most cases)
  • Use instanceof only when explicitly designed for inheritance

6. Modern Alternatives

Java Records (Java 16+):
JAVA(12 lines)
Code
Loading syntax highlighter...
Lombok @EqualsAndHashCode:
JAVA(17 lines)
Code
Loading syntax highlighter...
IDE-Generated:
JAVA(7 lines)
Code
Loading syntax highlighter...

7. Comparable and Comparator

The Comparable Contract:
JAVA(13 lines)
Code
Loading syntax highlighter...
Why "consistent with equals" matters:
JAVA(21 lines)
Code
Loading syntax highlighter...
Implementing Comparable:
JAVA(24 lines)
Code
Loading syntax highlighter...
Comparator for flexible sorting:
JAVA(17 lines)
Code
Loading syntax highlighter...

⚠️ Common Mistakes

Mistake 1: Mutable Fields in equals/hashCode

❌ Problem:
JAVA(18 lines)
Code
Loading syntax highlighter...
βœ… Solution:
JAVA(11 lines)
Code
Loading syntax highlighter...

Mistake 2: Using == for Object Comparison

❌ Problem:
JAVA(7 lines)
Code
Loading syntax highlighter...
βœ… Solution:
JAVA(2 lines)
Code
Loading syntax highlighter...

Mistake 3: Forgetting Null Handling

❌ Problem:
JAVA(5 lines)
Code
Loading syntax highlighter...
βœ… Solution:
JAVA(7 lines)
Code
Loading syntax highlighter...

Mistake 4: Inconsistent Fields in equals vs hashCode

❌ Problem:
JAVA(14 lines)
Code
Loading syntax highlighter...
βœ… Solution:
JAVA(10 lines)
Code
Loading syntax highlighter...

Mistake 5: Breaking Symmetry with Inheritance

❌ Problem:
JAVA(28 lines)
Code
Loading syntax highlighter...
βœ… Solution:
JAVA(12 lines)
Code
Loading syntax highlighter...

πŸ› Debug This: The Memory Leak

The application's memory usage grows continuously. After profiling, you find millions of Session objects in a HashSet<Session> that should have been removed:
JAVA(47 lines)
Code
Loading syntax highlighter...
Find the bug before reading the solution!

βœ… Solution:
The lastAccess field is mutable and included in equals() and hashCode(). When touch() is called:
  1. Session's lastAccess changes
  2. hashCode() returns different value
  3. Session is now in wrong bucket
  4. remove() looks in new bucket, doesn't find it
  5. Session is "stuck" in the old bucket β†’ memory leak
The fix:
JAVA(19 lines)
Code
Loading syntax highlighter...

πŸ’» Exercises

Exercise 1: Implement Employee equals/hashCode

⭐⭐ Difficulty: Medium | ⏱️ Time: 15 minutes

Task: Implement proper equals() and hashCode() for an Employee class.
JAVA(10 lines)
Code
Loading syntax highlighter...
βœ… Solution:
JAVA(42 lines)
Code
Loading syntax highlighter...

Exercise 2: Find the Bug

⭐⭐⭐ Difficulty: Medium | ⏱️ Time: 15 minutes

Task: This code has a bug in the equals/hashCode implementation. Find it and fix it.
JAVA(27 lines)
Code
Loading syntax highlighter...
βœ… Solution:

There are multiple bugs:

JAVA(28 lines)
Code
Loading syntax highlighter...
Bugs found:
  1. Missing this == o optimization
  2. Missing null check in equals (would NPE on null argument)
  3. namespace.equals() would NPE if namespace is null
  4. version in equals but NOT in hashCode (inconsistent fields!)
  5. Using + instead of proper hash combination (more collisions)

Exercise 3: Comparator Chain

⭐⭐ Difficulty: Medium | ⏱️ Time: 10 minutes

Task: Create comparators for sorting a list of products:
JAVA(13 lines)
Code
Loading syntax highlighter...
βœ… Solution:
JAVA(38 lines)
Code
Loading syntax highlighter...

Exercise 4: Value Object with Record

⭐⭐ Difficulty: Medium | ⏱️ Time: 10 minutes

Task: Convert this class to a Record and add validation:
JAVA(8 lines)
Code
Loading syntax highlighter...
βœ… Solution:
JAVA(39 lines)
Code
Loading syntax highlighter...

Exercise 5: Consistent with Equals

⭐⭐⭐⭐ Difficulty: Hard | ⏱️ Time: 20 minutes

Task: Implement a Version class that is Comparable and consistent with equals:
JAVA(11 lines)
Code
Loading syntax highlighter...
βœ… Solution:
JAVA(80 lines)
Code
Loading syntax highlighter...

πŸ“ Summary & Key Takeaways

The Contracts

MethodKey Properties
equals()Reflexive, Symmetric, Transitive, Consistent, null→false
hashCode()Consistent, equal objects→same hash
compareTo()Antisymmetric, Transitive, should be consistent with equals

The Golden Rules

  1. Override both or neither: If you override equals(), override hashCode()
  2. Use same fields: Include exactly the same fields in both methods
  3. Immutable fields only: Never include mutable fields in hash calculations
  4. Consistent with equals: compareTo() returning 0 should mean equals() is true

Quick Implementation Guide

JAVA(15 lines)
Code
Loading syntax highlighter...

Modern Alternatives

ApproachWhen to Use
RecordValue objects, DTOs, immutable data
LombokLegacy classes, entities with selected fields
IDE-generatedMost cases, review the output
ManualComplex inheritance, performance-critical code

🎀 Senior-Level Interview Questions

Question #1: What happens if you break the hashCode contract?

What interviewers want to hear: Understanding of how HashMap/HashSet work internally.
Strong answer: If equals() returns true but hashCode() returns different values, objects that should be "found" in HashMap/HashSet won't be. The collection uses hashCode to determine which bucket to look in. If two equal objects have different hashes, they'll be in different buckets, and contains() or get() will fail even though the object is present. Additionally, you can add "duplicates" to a HashSet because it doesn't find the existing equal object.

Question #2: Why shouldn't you use mutable objects as HashMap keys?

What interviewers want to hear: Understanding of the "lost object" problem.
Strong answer: If a key's hashCode changes after being added to a HashMap (because its mutable fields changed), the entry becomes "lost." It's still in the map but in a bucket that no longer matches its current hashCode. get() with the modified key looks in the wrong bucket and returns null. remove() also fails. The object is stuck, causing a memory leak. Even iterating won't help because you can't remove by key. The only solution is to rebuild the entire map.

Question #3: What's the difference between Comparable and Comparator?

What interviewers want to hear: Understanding of natural ordering vs external comparison.
Strong answer:
  • Comparable defines the natural ordering of a class. It's implemented by the class itself via compareTo(T other). There's only one natural ordering per class. Example: String is naturally ordered alphabetically.
  • Comparator is an external comparison strategy. It's a separate object that compares two instances via compare(T o1, T o2). You can have many comparators for different sort orders. Example: Sort employees by name, salary, or hire date using different Comparators.

Use Comparable for the obvious/default ordering, Comparator for alternative orderings.


Question #4: What does "consistent with equals" mean for Comparable?

What interviewers want to hear: Understanding of the relationship between comparison and equality.
Strong answer: A Comparable implementation is "consistent with equals" when (x.compareTo(y) == 0) == x.equals(y) for all objects. If two objects are equal according to compareTo() (returning 0), they should also be equal according to equals(), and vice versa.
This matters for sorted collections like TreeSet. TreeSet uses compareTo() for equality, not equals(). If they're inconsistent, TreeSet behavior differs from HashSet. Classic example: BigDecimal where
new BigDecimal("1.0").equals(new BigDecimal("1.00"))
is false (different scale), but compareTo() returns 0 (same numeric value).

Question #5: How do Java Records implement equals and hashCode?

What interviewers want to hear: Understanding of Records and automatic implementations.
Strong answer: Records automatically generate equals() and hashCode() based on all record components (the constructor parameters). The generated equals() compares all components for equality using Objects.equals(). The generated hashCode() combines all components using the same algorithm as Objects.hash().

Important: These implementations use ALL components. If you want to exclude certain fields from equality, you cannot use a Record - use a regular class instead. Also, record components should be immutable (or at least their identity should be), since they're all included in hashCode.


Question #6: When can two different objects have the same hashCode?

What interviewers want to hear: Understanding of hash collisions.
Strong answer: Any time - this is called a hash collision and is perfectly legal. The hashCode contract only requires that equal objects have the same hash, not that unequal objects have different hashes. With only 2^32 possible int values but infinitely many possible objects, collisions are mathematically inevitable (pigeonhole principle).
Good hash functions minimize collisions for better performance. Example: "FB".hashCode() == "Ea".hashCode() (both are 2236). HashMap handles this via chaining or tree bins for buckets with many collisions.

🏁 Conclusion

The equals() and hashCode() contract is fundamental to how Java collections work. Master it, and you'll:
  • Avoid subtle bugs where objects "disappear" from collections
  • Prevent memory leaks caused by stuck objects
  • Write correct entity classes that work properly in Sets and as Map keys
  • Ace technical interviews on this frequently-asked topic
Key principles to remember:
  1. Always override both equals() and hashCode() together
  2. Use only immutable fields that define object identity
  3. For entities, ID/primary key is usually sufficient
  4. Consider using Records for value objects
  5. When using Comparable, keep it consistent with equals
Your next step: Continue to Part 4 (Iterators) to understand how to traverse collections safely, or Part 12 (HashMap Internals) if you're on the Performance track.

πŸ“… Review Schedule for This Article

DayTaskTime
Day 1Review the golden rules and contracts5 min
Day 3Redo Exercise 1 (Employee equals/hashCode)15 min
Day 7Answer interview questions without looking10 min
Day 14Redo Debug This exercise (Memory Leak)10 min
Day 30Review mental model and quick reference5 min

Next in series: [Part 4: Iterators, Spliterators & Iteration Patterns]