Java equals() and hashCode() Methods
Java equals() and hashCode() Methods
Two objects that hold the same data can behave completely differently in Java depending on whether equals() and hashCode() are overridden. Store one in a HashSet, put the other in as a key for a HashMap lookup — and get nothing back, even though the data matches perfectly. The bug compiles without warnings and fails silently at runtime.
equals() and hashCode() are inherited from java.lang.Object. Their defaults work on reference identity — two objects are equal only if they are literally the same object in memory. For domain objects like User, Product, or Order, equality usually means "same data" — and that requires overriding both methods, always together, always consistently.
What equals() Does by Default
The default equals() in Object is == — reference comparison. Two references are equal only if they point to the exact same object.
// File: DefaultEqualsDemo.java
public class DefaultEqualsDemo {
static class Ticket {
String ticketId;
String event;
Ticket(String ticketId, String event) {
this.ticketId = ticketId;
this.event = event;
}
}
public static void main(String[] args) {
Ticket t1 = new Ticket("TKT-001", "IPL Final");
Ticket t2 = new Ticket("TKT-001", "IPL Final"); // same data, different object
Ticket t3 = t1; // same reference
System.out.println("t1 == t2 : " + (t1 == t2)); // false — different objects
System.out.println("t1.equals(t2) : " + t1.equals(t2)); // false — default is ==
System.out.println("t1 == t3 : " + (t1 == t3)); // true — same reference
System.out.println("t1.equals(t3) : " + t1.equals(t3)); // true — same reference
}
}Output:
t1 == t2 : false
t1.equals(t2) : false
t1 == t3 : true
t1.equals(t3) : true
t1 and t2 carry identical data. Without an override, equals() returns false because they are different objects in memory. For domain logic that treats two tickets with the same ID as the same ticket, this default is wrong.
What hashCode() Does by Default
The default hashCode() in Object returns an integer derived from the object's memory address — or a value seeded from it by the JVM. Two different objects almost always get different hash codes, even if they hold identical data.
// File: DefaultHashCodeDemo.java
public class DefaultHashCodeDemo {
static class Product {
String sku;
double price;
Product(String sku, double price) {
this.sku = sku;
this.price = price;
}
}
public static void main(String[] args) {
Product p1 = new Product("SKU-001", 1499.0);
Product p2 = new Product("SKU-001", 1499.0); // identical data
System.out.println("p1 hashCode: " + p1.hashCode());
System.out.println("p2 hashCode: " + p2.hashCode());
System.out.println("Same hash? " + (p1.hashCode() == p2.hashCode())); // almost always false
}
}Output:
p1 hashCode: 1922154895
p2 hashCode: 883049899
Same hash? false
Different hash codes mean HashMap and HashSet place these objects in different buckets. A lookup using p2 never finds p1 — even if equals() would say they match. This is why overriding equals() without hashCode() breaks hash-based collections silently.
The equals()-hashCode() Contract
Java defines a strict contract between these two methods. Every implementation must satisfy all three rules:
Rule 1 — Consistency with equality: If a.equals(b) returns true, then a.hashCode() must equal b.hashCode().
Rule 2 — Hash code stability: Calling hashCode() on the same object multiple times must return the same value within a single JVM session, as long as no field used in the computation changes.
Rule 3 — Inequality is not guaranteed: If a.equals(b) is false, a.hashCode() and b.hashCode() do not need to differ — hash collisions are allowed. But minimising collisions improves performance in hash-based collections.
The contract visualised: equals() = true → hashCode() MUST be equal (mandatory) equals() = false → hashCode() MAY be equal (allowed — collision) hashCode() same → equals() MAY be true or false (depends on collision) hashCode() differ → equals() MUST be false (different buckets = not equal)
Violating Rule 1 — overriding equals() without a matching hashCode() — is the single most common cause of silent data loss in Java applications.
Overriding equals() Correctly
A correct equals() implementation follows five steps in order. Skipping any one of them introduces a bug.
// File: OrderLine.java
import java.util.Objects;
public class OrderLine {
private final String productSku;
private final int quantity;
private final double unitPrice;
public OrderLine(String productSku, int quantity, double unitPrice) {
this.productSku = productSku;
this.quantity = quantity;
this.unitPrice = unitPrice;
}
public String getProductSku() { return productSku; }
public int getQuantity() { return quantity; }
public double getUnitPrice() { return unitPrice; }
@Override
public boolean equals(Object obj) {
// Step 1 — same reference: a.equals(a) must always be true
if (this == obj) return true;
// Step 2 — null check: a.equals(null) must always be false
if (obj == null) return false;
// Step 3 — type check with pattern matching (Java 16+)
// Use instanceof rather than getClass() for subclass compatibility
if (!(obj instanceof OrderLine other)) return false;
// Step 4 — field comparison: compare fields that define equality
// Objects.equals() handles null fields safely
return quantity == other.quantity
&& Double.compare(unitPrice, other.unitPrice) == 0
&& Objects.equals(productSku, other.productSku);
}
@Override
public int hashCode() {
// Step 5 — hashCode must use the same fields as equals()
return Objects.hash(productSku, quantity, unitPrice);
}
@Override
public String toString() {
return "OrderLine{sku='" + productSku + "', qty=" + quantity
+ ", price=Rs." + unitPrice + "}";
}
}// File: EqualsCorrectDemo.java
public class EqualsCorrectDemo {
public static void main(String[] args) {
OrderLine line1 = new OrderLine("SKU-101", 2, 999.0);
OrderLine line2 = new OrderLine("SKU-101", 2, 999.0);
OrderLine line3 = new OrderLine("SKU-101", 3, 999.0); // different quantity
OrderLine line4 = new OrderLine("SKU-202", 2, 999.0); // different SKU
System.out.println("line1.equals(line2) : " + line1.equals(line2)); // true
System.out.println("line1.equals(line3) : " + line1.equals(line3)); // false — qty differs
System.out.println("line1.equals(line4) : " + line1.equals(line4)); // false — sku differs
System.out.println("line1.equals(null) : " + line1.equals(null)); // false — null safe
System.out.println("line1.equals(line1) : " + line1.equals(line1)); // true — reflexive
System.out.println("\nhashCode same for equal objects: "
+ (line1.hashCode() == line2.hashCode())); // true — contract satisfied
}
}Output:
line1.equals(line2) : true
line1.equals(line3) : false — qty differs
line1.equals(line4) : false — sku differs
line1.equals(null) : false — null safe
line1.equals(line1) : true — reflexive
hashCode same for equal objects: true
Double.compare(a, b) == 0 is used instead of a == b for double fields because of floating-point precision — two values that are logically equal might not pass == due to representation differences. Objects.equals() handles null String fields safely without a separate null check.
How HashMap Uses equals() and hashCode() Together
HashMap uses both methods in a two-step lookup. Understanding this explains every hash-collection bug.
HashMap.put(key, value):
1. Compute key.hashCode()
2. Apply bit manipulation → determine bucket index
3. Navigate to that bucket
4. If bucket is empty → store entry
5. If bucket has entries → call key.equals() on each entry
→ if equals() returns true → update value
→ if no match → add new entry (collision handling)
HashMap.get(key):
1. Compute key.hashCode() ← must match the hashCode() used during put
2. Navigate to that bucket ← wrong bucket = nothing found
3. Call key.equals() on each entry in the bucket
4. Return value if found, null if not
If hashCode() produces a different value at get() time than at put() time
→ lookup goes to a different bucket
→ get() returns null even though the key exists in the map
// File: HashMapMechanicsDemo.java
import java.util.*;
public class HashMapMechanicsDemo {
// Breaks the contract — equals overridden but hashCode is not
static class BrokenKey {
String id;
BrokenKey(String id) { this.id = id; }
@Override
public boolean equals(Object obj) {
if (!(obj instanceof BrokenKey other)) return false;
return Objects.equals(id, other.id);
}
// hashCode not overridden — different objects get different hash codes
}
// Correct contract — both overridden consistently
static class CorrectKey {
final String id;
CorrectKey(String id) { this.id = id; }
@Override
public boolean equals(Object obj) {
if (!(obj instanceof CorrectKey other)) return false;
return Objects.equals(id, other.id);
}
@Override
public int hashCode() { return Objects.hash(id); }
}
public static void main(String[] args) {
// Broken — put with one object, get with another equal object
Map<BrokenKey, String> brokenMap = new HashMap<>();
BrokenKey bk1 = new BrokenKey("USR-001");
BrokenKey bk2 = new BrokenKey("USR-001"); // equal by our equals(), different hashCode
brokenMap.put(bk1, "Admin");
System.out.println("Broken map:");
System.out.println(" put key hashCode : " + bk1.hashCode());
System.out.println(" get key hashCode : " + bk2.hashCode());
System.out.println(" get result : " + brokenMap.get(bk2)); // null — wrong bucket
// Correct — put and get go to the same bucket
Map<CorrectKey, String> correctMap = new HashMap<>();
CorrectKey ck1 = new CorrectKey("USR-001");
CorrectKey ck2 = new CorrectKey("USR-001");
correctMap.put(ck1, "Admin");
System.out.println("\nCorrect map:");
System.out.println(" put key hashCode : " + ck1.hashCode());
System.out.println(" get key hashCode : " + ck2.hashCode());
System.out.println(" get result : " + correctMap.get(ck2)); // Admin — correct
// HashSet deduplication also depends on both methods
Set<CorrectKey> keySet = new HashSet<>();
keySet.add(ck1);
keySet.add(ck2); // duplicate — not added
System.out.println("\nHashSet size (should be 1): " + keySet.size());
}
}Output:
Broken map:
put key hashCode : 1922154895
get key hashCode : 883049899
get result : null
Correct map:
put key hashCode : 1031904321
get key hashCode : 1031904321
get result : Admin
HashSet size (should be 1): 1
The broken class produces different hash codes for equal objects. The put stores the entry in bucket A. The get computes a different hash, goes to bucket B, finds nothing, and returns null. The CorrectKey produces the same hash for equal objects — both put and get land in the same bucket, and equals() confirms the match.
equals() vs == — Comparison Table
| Aspect | == operator | equals() method |
|---|---|---|
| What it checks | Memory address — reference identity | Logical equality — depends on override |
| Type | Language operator | Instance method from Object |
| For primitives | Compares values directly | Not applicable — primitives have no methods |
| For objects (default) | Same object in memory | Same as == — reference identity |
| For objects (overridden) | Still reference identity | Field-based comparison — defined by class |
| Can be overridden | No — operator is fixed | Yes — every class can define its own |
| Null safety | null == null is true | Calling on null throws NullPointerException |
| String behaviour | Compares references — often wrong | Compares character content — correct |
Used by HashMap/HashSet | Never | Always — with hashCode() |
| Typical use | Primitives, reference identity checks | Domain equality between objects |
equals() Contracts — The Five Properties
A correct equals() implementation must satisfy five mathematical properties. The JVM does not enforce these — violating them causes subtle, hard-to-diagnose failures in sorted collections, deduplication logic, and caching layers.
| Property | Definition | Violation consequence |
|---|---|---|
| Reflexive | a.equals(a) is always true | Object cannot be found in its own collection |
| Symmetric | If a.equals(b) is true, then b.equals(a) must also be true | contains() gives different results depending on which object calls it |
| Transitive | If a.equals(b) and b.equals(c), then a.equals(c) | Deduplication logic fails — same element counted multiple times |
| Consistent | Repeated calls return the same result if no fields change | Unpredictable behaviour in caches and sorted sets |
| Null-safe | a.equals(null) must return false — never throw | NullPointerException inside library code that calls equals |
Using Objects.equals() and Objects.hash()
java.util.Objects provides utility methods that handle null safely and reduce boilerplate. Every production equals() and hashCode() implementation should use them.
// File: ObjectsUtilityDemo.java
import java.util.Objects;
public class ObjectsUtilityDemo {
static class Customer {
private final String customerId;
private final String email;
private final String phone; // might be null — not all customers provide it
Customer(String customerId, String email, String phone) {
this.customerId = customerId;
this.email = email;
this.phone = phone;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Customer other)) return false;
// Objects.equals() handles null without throwing
return Objects.equals(customerId, other.customerId)
&& Objects.equals(email, other.email);
// phone excluded from equality — two accounts with same ID
// and email are the same customer regardless of phone
}
@Override
public int hashCode() {
// Objects.hash() handles null fields and computes a combined hash
return Objects.hash(customerId, email);
// phone excluded — must match equals()
}
@Override
public String toString() {
return "Customer{id='" + customerId + "', email='" + email + "'}";
}
}
public static void main(String[] args) {
Customer c1 = new Customer("CUST-101", "priya@zepto.in", "9876543210");
Customer c2 = new Customer("CUST-101", "priya@zepto.in", null); // no phone
Customer c3 = new Customer("CUST-102", "rahul@zepto.in", "9123456789");
System.out.println("c1.equals(c2) : " + c1.equals(c2)); // true — phone not in equals
System.out.println("c1.equals(c3) : " + c1.equals(c3)); // false — different ID
System.out.println("c1.hashCode() == c2.hashCode() : "
+ (c1.hashCode() == c2.hashCode())); // true — contract satisfied
// Demonstrating Objects.equals() safety
System.out.println("\nObjects.equals null handling:");
System.out.println("Objects.equals(null, null) : " + Objects.equals(null, null)); // true
System.out.println("Objects.equals(c1, null) : " + Objects.equals(c1, null)); // false
System.out.println("Objects.equals(null, c1) : " + Objects.equals(null, c1)); // false
}
}Output:
c1.equals(c2) : true
c1.equals(c3) : false
c1.hashCode() == c2.hashCode() : true
Objects.equals null handling:
Objects.equals(null, null) : true
Objects.equals(c1, null) : false
Objects.equals(null, c1) : false
Objects.equals(a, b) internally checks a == b first (handles same reference and both-null), then calls a.equals(b) — it never throws even if a is null. Objects.hash(field1, field2, ...) computes a combined hash with null-safe field handling, using a prime-multiplication formula that distributes hash codes evenly across buckets.
Basic Example
public class Main {
static class Book {
String title;
String author;
Book(String title, String author) {
this.title = title;
this.author = author;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Book other)) return false;
return title.equals(other.title) && author.equals(other.author);
}
@Override
public int hashCode() {
return java.util.Objects.hash(title, author);
}
}
public static void main(String[] args) {
Book b1 = new Book("Java Basics", "Anita");
Book b2 = new Book("Java Basics", "Anita"); // same data, different object
System.out.println("b1 == b2 : " + (b1 == b2));
System.out.println("b1.equals(b2) : " + b1.equals(b2));
System.out.println("Same hashCode : " + (b1.hashCode() == b2.hashCode()));
java.util.Set<Book> library = new java.util.HashSet<>();
library.add(b1);
library.add(b2); // treated as a duplicate
System.out.println("Books in library: " + library.size());
}
}Output
b1 == b2 : false b1.equals(b2) : true Same hashCode : true Books in library: 1
b1 and b2 are two separate objects in memory, so == returns false. Because Book overrides equals() to compare title and author, b1.equals(b2) returns true. hashCode() is overridden using the same fields, so both objects produce the same hash — satisfying the contract. This is why the HashSet correctly treats b2 as a duplicate of b1 and keeps only one entry.
Best Practices
Always override hashCode() when you override equals() — and vice versa. The IDE will offer to generate both together. Any static analysis tool — SpotBugs, Checkstyle, SonarQube — flags an equals() without hashCode(). The consequences of getting this wrong are silent data loss in collections, not a compilation error or an obvious runtime crash.
Use the same fields in both methods. The contract demands it: if two objects are equal, their hash codes must match. If equals() compares id and email but hashCode() only uses id, two objects with the same id but different email get the same hash but compare as unequal — valid but wasteful. If equals() uses id but hashCode() uses email, objects that are equal may get different hashes — contract violated.
Prefer immutable fields for equals() and hashCode(). An object that is used as a HashMap key and whose key fields are mutated after insertion becomes unfindable — the map stores it in one bucket, but the new hash points to a different bucket. Use final fields, or at minimum, never mutate the fields that participate in hashCode() after the object enters a collection.
Use Objects.equals() for field comparison and Objects.hash() for hash computation. Both handle null safely without extra null checks. Objects.hash() uses a well-distributed prime-multiplication formula. Manual implementations using 31 * result + field.hashCode() work but are more error-prone to write and harder to read.
Common Mistakes
Mistake 1 — Overriding equals() Without hashCode()
public class Session {
private String sessionId;
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Session other)) return false;
return Objects.equals(sessionId, other.sessionId);
}
// hashCode() not overridden
// Two equal Session objects have different hash codes
// HashSet<Session> accumulates duplicates silently
// HashMap<Session, ?> returns null for equal keys — silently
}This is the single most common Object method mistake in Java codebases. It compiles without warning and fails only when the object is used in a hash-based collection — which may not happen until the code is in production.
Mistake 2 — Using Different Fields in equals() and hashCode()
public class Order {
private String orderId;
private String customerId;
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Order other)) return false;
return Objects.equals(orderId, other.orderId)
&& Objects.equals(customerId, other.customerId); // two fields
}
@Override
public int hashCode() {
return Objects.hash(orderId); // only one field — contract risk
}
}If two orders have the same orderId but different customerId, equals() returns false but they share the same hashCode(). This is a hash collision — technically allowed but produces poor performance in large collections. More dangerously, if the fields are reversed — equals() uses fewer fields than hashCode() — two objects that equals() considers equal might have different hash codes, which breaks the contract entirely.
Mistake 3 — Mutating hashCode() Fields After Inserting Into a Collection
Map<Product, Integer> stockMap = new HashMap<>();
Product p = new Product("SKU-001", "Headphones");
stockMap.put(p, 150);
p.setSku("SKU-999"); // mutates the field used in hashCode()
// HashMap stored p in the bucket for "SKU-001" hash
// Now p.hashCode() returns the bucket for "SKU-999" hash
System.out.println(stockMap.get(p)); // null — p is in the wrong bucket now
System.out.println(stockMap.containsValue(150)); // true — it is there, just unfindableThe entry is still in the map. The key object is still in its original bucket. But every subsequent lookup computes the new hash, lands in the wrong bucket, and finds nothing. Use immutable fields — or final fields — in equals() and hashCode() to prevent this.
Mistake 4 — Using getClass() Instead of instanceof in equals()
// Too strict — breaks subclass equality
@Override
public boolean equals(Object obj) {
if (obj == null || getClass() != obj.getClass()) return false;
Order other = (Order) obj;
return Objects.equals(orderId, other.orderId);
}
// Problem — if a subclass overrides nothing but equals uses getClass():
// Order o1 = new Order("ORD-001");
// Order o2 = new PriorityOrder("ORD-001"); // subclass
// o1.equals(o2) → false — getClass() differs
// o2.equals(o1) → false — getClass() differs
// Lists.contains() fails even though they represent the same ordergetClass() equality breaks when subclasses are involved and should be considered the same entity. instanceof allows a PriorityOrder to be equal to an Order if they have the same ID, which is usually what domain code expects. Use instanceof with pattern matching unless you have a specific reason to require exact class identity.
Interview Questions
Q1. What is the contract between equals() and hashCode() in Java?
If two objects are equal according to equals(), they must return the same value from hashCode(). This is mandatory. The reverse is not required — objects with the same hash code are not necessarily equal (hash collisions are allowed). Violating the contract by overriding equals() without a matching hashCode() causes HashMap and HashSet to behave incorrectly: equal objects land in different buckets, lookups return null for existing keys, and HashSet stores duplicates silently.
Q2. What happens if you override equals() but not hashCode()?
Two objects that are equal by equals() will produce different hash codes — derived from their memory addresses by the default Object.hashCode(). When one is stored in a HashMap or HashSet and the other is used for lookup, the different hash codes send the lookup to a different bucket. The entry is never found. HashSet.contains() returns false for an equal object. HashMap.get() returns null even though the key exists. The bug is silent — no exception, no warning, just wrong results.
Q3. Why should you use Objects.equals() and Objects.hash() instead of writing equals and hashCode manually?
Objects.equals(a, b) handles null safely — it returns true if both are null, false if one is null, and a.equals(b) otherwise. Without it, each nullable field requires a null check inside equals(). Objects.hash(fields...) computes a consistent combined hash using a prime-multiplication formula, handles null fields safely, and makes the implementation one readable line. Manual prime-multiplication formulas are error-prone and harder to maintain. Both utilities produce correct, production-quality implementations with minimal code.
Q4. What is the difference between == and equals() in Java?
== on objects compares memory addresses — it returns true only when both references point to the exact same object. equals() compares logical equality — the meaning of "equal" is defined by the class override. For String, equals() compares character content. For a custom Order, it might compare only the order ID. The key rule: never use == to compare object content. Always use equals(), and ensure equals() is properly overridden to express the correct notion of equality for that domain object.
Q5. Can two objects have the same hashCode() but return false for equals()?
Yes. This is called a hash collision and is explicitly allowed by the contract. The hashCode() function maps a potentially infinite set of objects to a finite set of integers — collisions are mathematically inevitable. When two unequal objects share a bucket in a HashMap, equals() is called to distinguish them. Many hash collisions degrade HashMap performance from O(1) to O(n) but do not cause logical errors. What causes logical errors is the reverse: two equal objects with different hash codes — which violates the contract and breaks all hash-based collections.
Q6. Why should fields used in equals() and hashCode() be immutable or final?
If a field used in hashCode() is mutated after an object is inserted into a HashMap or HashSet, the object's new hash code points to a different bucket than the one where it was stored. Lookups compute the new hash, find the wrong bucket, and return null or false — even though the object is still in the collection. Making the fields final prevents this mutation entirely. For fields that must be mutable, the only safe approach is to remove the object from the collection before mutation and re-insert it after.
FAQs
Do I need to override equals() and hashCode() for every class?
No — only for classes that represent domain entities where equality is based on data rather than identity, and especially for any class used as a HashMap key or stored in a HashSet. Utility classes, service classes, and classes that are never compared or stored in hash collections can safely use the Object defaults.
Does Java automatically generate equals() and hashCode() for Records?
Yes. Records introduced in Java 16 automatically generate equals(), hashCode(), and toString() based on all declared components. Two records are equal if all their components are equal. The auto-generated hashCode() is consistent with equals(). You can override them in the record body if the auto-generated behaviour does not fit your requirements.
Can equals() be asymmetric between a parent class and a subclass?
Yes, and this is a known design risk. If Animal.equals() uses instanceof Animal and Dog.equals() uses instanceof Dog, then animal.equals(dog) might return true while dog.equals(animal) returns false — violating the symmetry property. The standard advice is: when classes that extend each other need to define equality, use either getClass() for strict type matching, or ensure both sides use the same instanceof check consistently.
What is the impact of a poor hashCode() implementation on HashMap performance?
A poor hashCode() that returns the same value for all objects — like return 1 — forces every entry into a single bucket. HashMap degrades from O(1) average-case lookup to O(n) because every lookup must traverse the entire chain of collisions in that one bucket. From Java 8 onwards, Java converts long collision chains to balanced binary trees, improving the worst case to O(log n), but this is still far worse than a well-distributed hash function.
Should equals() throw exceptions for unexpected types?
No. equals() should return false for any object that is not of a compatible type — never throw an exception. This is part of the null-safety and type-safety contract. The instanceof check at the start of equals() handles both null (returns false for null) and wrong types (returns false for anything not an instance of the class) gracefully.
Summary
equals() and hashCode() are inseparable. Override one without the other and every hash-based collection in the Java standard library silently produces wrong results. The contract is non-negotiable: equal objects must produce equal hash codes. The implementation pattern is always the same — five steps in equals(), matching fields in hashCode(), both using Objects.equals() and Objects.hash() for null safety.
The most important production rule is to use immutable or final fields in both methods. Mutable key fields in a HashMap or HashSet produce objects that become permanently unfindable after mutation — no exception, no warning, just silent data loss. Records handle all of this automatically from Java 16 onwards, which is why they are the preferred choice for immutable value objects in modern Java.
For interviews, be ready to explain the contract precisely, demonstrate a correct five-step equals() implementation from memory, explain why HashMap.get() returns null when hashCode() is not overridden, and describe what happens when fields used in hashCode() are mutated after insertion. These appear in nearly every Java interview at both service-based and product-based companies.
What to Read Next
Practice Programs
Put this topic into practice — each program has full code, output and a step-by-step explanation you can run online.
Overrides equals() to compare two objects by their field values instead of reference identity.
Overrides hashCode() so it stays consistent with an equals() override.