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.
1// File: DefaultEqualsDemo.java
2
3public class DefaultEqualsDemo {
4
5 static class Ticket {
6 String ticketId;
7 String event;
8
9 Ticket(String ticketId, String event) {
10 this.ticketId = ticketId;
11 this.event = event;
12 }
13 }
14
15 public static void main(String[] args) {
16
17 Ticket t1 = new Ticket("TKT-001", "IPL Final");
18 Ticket t2 = new Ticket("TKT-001", "IPL Final"); // same data, different object
19 Ticket t3 = t1; // same reference
20
21 System.out.println("t1 == t2 : " + (t1 == t2)); // false — different objects
22 System.out.println("t1.equals(t2) : " + t1.equals(t2)); // false — default is ==
23 System.out.println("t1 == t3 : " + (t1 == t3)); // true — same reference
24 System.out.println("t1.equals(t3) : " + t1.equals(t3)); // true — same reference
25 }
26}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.
1// File: DefaultHashCodeDemo.java
2
3public class DefaultHashCodeDemo {
4
5 static class Product {
6 String sku;
7 double price;
8
9 Product(String sku, double price) {
10 this.sku = sku;
11 this.price = price;
12 }
13 }
14
15 public static void main(String[] args) {
16
17 Product p1 = new Product("SKU-001", 1499.0);
18 Product p2 = new Product("SKU-001", 1499.0); // identical data
19
20 System.out.println("p1 hashCode: " + p1.hashCode());
21 System.out.println("p2 hashCode: " + p2.hashCode());
22 System.out.println("Same hash? " + (p1.hashCode() == p2.hashCode())); // almost always false
23 }
24}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.
1// File: OrderLine.java
2
3import java.util.Objects;
4
5public class OrderLine {
6
7 private final String productSku;
8 private final int quantity;
9 private final double unitPrice;
10
11 public OrderLine(String productSku, int quantity, double unitPrice) {
12 this.productSku = productSku;
13 this.quantity = quantity;
14 this.unitPrice = unitPrice;
15 }
16
17 public String getProductSku() { return productSku; }
18 public int getQuantity() { return quantity; }
19 public double getUnitPrice() { return unitPrice; }
20
21 @Override
22 public boolean equals(Object obj) {
23 // Step 1 — same reference: a.equals(a) must always be true
24 if (this == obj) return true;
25
26 // Step 2 — null check: a.equals(null) must always be false
27 if (obj == null) return false;
28
29 // Step 3 — type check with pattern matching (Java 16+)
30 // Use instanceof rather than getClass() for subclass compatibility
31 if (!(obj instanceof OrderLine other)) return false;
32
33 // Step 4 — field comparison: compare fields that define equality
34 // Objects.equals() handles null fields safely
35 return quantity == other.quantity
36 && Double.compare(unitPrice, other.unitPrice) == 0
37 && Objects.equals(productSku, other.productSku);
38 }
39
40 @Override
41 public int hashCode() {
42 // Step 5 — hashCode must use the same fields as equals()
43 return Objects.hash(productSku, quantity, unitPrice);
44 }
45
46 @Override
47 public String toString() {
48 return "OrderLine{sku='" + productSku + "', qty=" + quantity
49 + ", price=Rs." + unitPrice + "}";
50 }
51}1// File: EqualsCorrectDemo.java
2
3public class EqualsCorrectDemo {
4
5 public static void main(String[] args) {
6
7 OrderLine line1 = new OrderLine("SKU-101", 2, 999.0);
8 OrderLine line2 = new OrderLine("SKU-101", 2, 999.0);
9 OrderLine line3 = new OrderLine("SKU-101", 3, 999.0); // different quantity
10 OrderLine line4 = new OrderLine("SKU-202", 2, 999.0); // different SKU
11
12 System.out.println("line1.equals(line2) : " + line1.equals(line2)); // true
13 System.out.println("line1.equals(line3) : " + line1.equals(line3)); // false — qty differs
14 System.out.println("line1.equals(line4) : " + line1.equals(line4)); // false — sku differs
15 System.out.println("line1.equals(null) : " + line1.equals(null)); // false — null safe
16 System.out.println("line1.equals(line1) : " + line1.equals(line1)); // true — reflexive
17
18 System.out.println("\nhashCode same for equal objects: "
19 + (line1.hashCode() == line2.hashCode())); // true — contract satisfied
20 }
21}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
1// File: HashMapMechanicsDemo.java
2
3import java.util.*;
4
5public class HashMapMechanicsDemo {
6
7 // Breaks the contract — equals overridden but hashCode is not
8 static class BrokenKey {
9 String id;
10 BrokenKey(String id) { this.id = id; }
11
12 @Override
13 public boolean equals(Object obj) {
14 if (!(obj instanceof BrokenKey other)) return false;
15 return Objects.equals(id, other.id);
16 }
17 // hashCode not overridden — different objects get different hash codes
18 }
19
20 // Correct contract — both overridden consistently
21 static class CorrectKey {
22 final String id;
23 CorrectKey(String id) { this.id = id; }
24
25 @Override
26 public boolean equals(Object obj) {
27 if (!(obj instanceof CorrectKey other)) return false;
28 return Objects.equals(id, other.id);
29 }
30
31 @Override
32 public int hashCode() { return Objects.hash(id); }
33 }
34
35 public static void main(String[] args) {
36
37 // Broken — put with one object, get with another equal object
38 Map<BrokenKey, String> brokenMap = new HashMap<>();
39 BrokenKey bk1 = new BrokenKey("USR-001");
40 BrokenKey bk2 = new BrokenKey("USR-001"); // equal by our equals(), different hashCode
41
42 brokenMap.put(bk1, "Admin");
43 System.out.println("Broken map:");
44 System.out.println(" put key hashCode : " + bk1.hashCode());
45 System.out.println(" get key hashCode : " + bk2.hashCode());
46 System.out.println(" get result : " + brokenMap.get(bk2)); // null — wrong bucket
47
48 // Correct — put and get go to the same bucket
49 Map<CorrectKey, String> correctMap = new HashMap<>();
50 CorrectKey ck1 = new CorrectKey("USR-001");
51 CorrectKey ck2 = new CorrectKey("USR-001");
52
53 correctMap.put(ck1, "Admin");
54 System.out.println("\nCorrect map:");
55 System.out.println(" put key hashCode : " + ck1.hashCode());
56 System.out.println(" get key hashCode : " + ck2.hashCode());
57 System.out.println(" get result : " + correctMap.get(ck2)); // Admin — correct
58
59 // HashSet deduplication also depends on both methods
60 Set<CorrectKey> keySet = new HashSet<>();
61 keySet.add(ck1);
62 keySet.add(ck2); // duplicate — not added
63 System.out.println("\nHashSet size (should be 1): " + keySet.size());
64 }
65}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.
1// File: ObjectsUtilityDemo.java
2
3import java.util.Objects;
4
5public class ObjectsUtilityDemo {
6
7 static class Customer {
8 private final String customerId;
9 private final String email;
10 private final String phone; // might be null — not all customers provide it
11
12 Customer(String customerId, String email, String phone) {
13 this.customerId = customerId;
14 this.email = email;
15 this.phone = phone;
16 }
17
18 @Override
19 public boolean equals(Object obj) {
20 if (this == obj) return true;
21 if (!(obj instanceof Customer other)) return false;
22
23 // Objects.equals() handles null without throwing
24 return Objects.equals(customerId, other.customerId)
25 && Objects.equals(email, other.email);
26 // phone excluded from equality — two accounts with same ID
27 // and email are the same customer regardless of phone
28 }
29
30 @Override
31 public int hashCode() {
32 // Objects.hash() handles null fields and computes a combined hash
33 return Objects.hash(customerId, email);
34 // phone excluded — must match equals()
35 }
36
37 @Override
38 public String toString() {
39 return "Customer{id='" + customerId + "', email='" + email + "'}";
40 }
41 }
42
43 public static void main(String[] args) {
44
45 Customer c1 = new Customer("CUST-101", "priya@zepto.in", "9876543210");
46 Customer c2 = new Customer("CUST-101", "priya@zepto.in", null); // no phone
47 Customer c3 = new Customer("CUST-102", "rahul@zepto.in", "9123456789");
48
49 System.out.println("c1.equals(c2) : " + c1.equals(c2)); // true — phone not in equals
50 System.out.println("c1.equals(c3) : " + c1.equals(c3)); // false — different ID
51 System.out.println("c1.hashCode() == c2.hashCode() : "
52 + (c1.hashCode() == c2.hashCode())); // true — contract satisfied
53
54 // Demonstrating Objects.equals() safety
55 System.out.println("\nObjects.equals null handling:");
56 System.out.println("Objects.equals(null, null) : " + Objects.equals(null, null)); // true
57 System.out.println("Objects.equals(c1, null) : " + Objects.equals(c1, null)); // false
58 System.out.println("Objects.equals(null, c1) : " + Objects.equals(null, c1)); // false
59 }
60}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.
Real-World Example — Inventory Management System
The Business Problem
An inventory system at a platform like Flipkart or Meesho tracks warehouse stock by product SKU and warehouse location. Products are stored in HashSet for deduplication, in HashMap<Product, Integer> for stock counts, and in sorted structures for reporting. Without correct equals() and hashCode(), duplicate stock entries accumulate silently, lookups return null for products that clearly exist, and stock counts become inconsistent between warehouses.
1// File: WarehouseLocation.java
2
3import java.util.Objects;
4
5public class WarehouseLocation {
6
7 private final String warehouseCode;
8 private final String zone;
9
10 public WarehouseLocation(String warehouseCode, String zone) {
11 this.warehouseCode = warehouseCode;
12 this.zone = zone;
13 }
14
15 public String getWarehouseCode() { return warehouseCode; }
16 public String getZone() { return zone; }
17
18 @Override
19 public boolean equals(Object obj) {
20 if (this == obj) return true;
21 if (!(obj instanceof WarehouseLocation other)) return false;
22 return Objects.equals(warehouseCode, other.warehouseCode)
23 && Objects.equals(zone, other.zone);
24 }
25
26 @Override
27 public int hashCode() {
28 return Objects.hash(warehouseCode, zone);
29 }
30
31 @Override
32 public String toString() {
33 return warehouseCode + "-" + zone;
34 }
35}1// File: StockItem.java
2
3import java.util.Objects;
4
5public class StockItem {
6
7 private final String sku;
8 private final String productName;
9 private final String category;
10 private final WarehouseLocation location;
11
12 public StockItem(String sku, String productName,
13 String category, WarehouseLocation location) {
14 Objects.requireNonNull(sku, "SKU cannot be null");
15 Objects.requireNonNull(location, "Location cannot be null");
16
17 this.sku = sku;
18 this.productName = productName;
19 this.category = category;
20 this.location = location;
21 }
22
23 public String getSku() { return sku; }
24 public String getProductName() { return productName; }
25 public WarehouseLocation getLocation() { return location; }
26
27 // Two stock items are the same if they are the same SKU at the same location
28 @Override
29 public boolean equals(Object obj) {
30 if (this == obj) return true;
31 if (!(obj instanceof StockItem other)) return false;
32 return Objects.equals(sku, other.sku)
33 && Objects.equals(location, other.location);
34 }
35
36 @Override
37 public int hashCode() {
38 return Objects.hash(sku, location); // location's hashCode() is used here
39 }
40
41 @Override
42 public String toString() {
43 return "StockItem{sku='" + sku
44 + "', product='" + productName
45 + "', location=" + location
46 + '}';
47 }
48}1// File: InventoryDemo.java
2
3import java.util.*;
4
5public class InventoryDemo {
6
7 public static void main(String[] args) {
8
9 WarehouseLocation mumbaiA = new WarehouseLocation("MUM-01", "Zone-A");
10 WarehouseLocation mumbaiB = new WarehouseLocation("MUM-01", "Zone-B");
11 WarehouseLocation delhiA = new WarehouseLocation("DEL-01", "Zone-A");
12
13 StockItem item1 = new StockItem("SKU-001", "Wireless Headphones", "Electronics", mumbaiA);
14 StockItem item2 = new StockItem("SKU-001", "Wireless Headphones", "Electronics", mumbaiA);
15 StockItem item3 = new StockItem("SKU-001", "Wireless Headphones", "Electronics", mumbaiB);
16 StockItem item4 = new StockItem("SKU-002", "Mechanical Keyboard", "Electronics", mumbaiA);
17 StockItem item5 = new StockItem("SKU-001", "Wireless Headphones", "Electronics", delhiA);
18
19 System.out.println("=== equals() checks ===");
20 System.out.println("item1.equals(item2): " + item1.equals(item2)); // true — same SKU + loc
21 System.out.println("item1.equals(item3): " + item1.equals(item3)); // false — same SKU, diff loc
22 System.out.println("item1.equals(item4): " + item1.equals(item4)); // false — diff SKU
23 System.out.println("item1.equals(item5): " + item1.equals(item5)); // false — same SKU, diff WH
24
25 System.out.println("\n=== HashSet deduplication ===");
26 Set<StockItem> inventory = new HashSet<>();
27 inventory.add(item1);
28 inventory.add(item2); // duplicate of item1 — not added
29 inventory.add(item3);
30 inventory.add(item4);
31 inventory.add(item5);
32 System.out.println("Unique stock entries: " + inventory.size()); // 4
33
34 System.out.println("\n=== HashMap stock counts ===");
35 Map<StockItem, Integer> stockCounts = new HashMap<>();
36 stockCounts.put(item1, 150);
37 stockCounts.put(item3, 80);
38 stockCounts.put(item4, 45);
39 stockCounts.put(item5, 200);
40
41 // Lookup using item2 — different object, same SKU and location as item1
42 System.out.println("Stock of item1 via item2 : " + stockCounts.get(item2)); // 150
43
44 System.out.println("\n=== Full inventory ===");
45 stockCounts.forEach((item, count) ->
46 System.out.println(" " + item + " | Qty: " + count));
47
48 System.out.println("\n=== hashCode consistency ===");
49 System.out.println("item1 hashCode: " + item1.hashCode());
50 System.out.println("item2 hashCode: " + item2.hashCode());
51 System.out.println("Contract satisfied: " + (item1.hashCode() == item2.hashCode()));
52 }
53}Output:
=== equals() checks ===
item1.equals(item2): true
item1.equals(item3): false
item1.equals(item4): false
item1.equals(item5): false
=== HashSet deduplication ===
Unique stock entries: 4
=== HashMap stock counts ===
Stock of item1 via item2 : 150
=== Full inventory ===
StockItem{sku='SKU-001', product='Wireless Headphones', location=MUM-01-Zone-A} | Qty: 150
StockItem{sku='SKU-001', product='Wireless Headphones', location=MUM-01-Zone-B} | Qty: 80
StockItem{sku='SKU-002', product='Mechanical Keyboard', location=MUM-01-Zone-A} | Qty: 45
StockItem{sku='SKU-001', product='Wireless Headphones', location=DEL-01-Zone-A} | Qty: 200
=== hashCode consistency ===
item1 hashCode: -1645724700
item2 hashCode: -1645724700
Contract satisfied: true
item2 is a separate object from item1 but represents the same stock entry — same SKU at the same location. The HashMap correctly returns 150 when looking up with item2, and the HashSet correctly keeps only 4 unique entries. The nested WarehouseLocation participates in both equals() and hashCode() of StockItem — and because WarehouseLocation also correctly overrides both methods, the combined hash works correctly for nested objects.
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()
1public class Session {
2 private String sessionId;
3
4 @Override
5 public boolean equals(Object obj) {
6 if (!(obj instanceof Session other)) return false;
7 return Objects.equals(sessionId, other.sessionId);
8 }
9 // hashCode() not overridden
10 // Two equal Session objects have different hash codes
11 // HashSet<Session> accumulates duplicates silently
12 // HashMap<Session, ?> returns null for equal keys — silently
13}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()
1public class Order {
2 private String orderId;
3 private String customerId;
4
5 @Override
6 public boolean equals(Object obj) {
7 if (!(obj instanceof Order other)) return false;
8 return Objects.equals(orderId, other.orderId)
9 && Objects.equals(customerId, other.customerId); // two fields
10 }
11
12 @Override
13 public int hashCode() {
14 return Objects.hash(orderId); // only one field — contract risk
15 }
16}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
1Map<Product, Integer> stockMap = new HashMap<>();
2Product p = new Product("SKU-001", "Headphones");
3stockMap.put(p, 150);
4
5p.setSku("SKU-999"); // mutates the field used in hashCode()
6
7// HashMap stored p in the bucket for "SKU-001" hash
8// Now p.hashCode() returns the bucket for "SKU-999" hash
9System.out.println(stockMap.get(p)); // null — p is in the wrong bucket now
10System.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()
1// Too strict — breaks subclass equality
2@Override
3public boolean equals(Object obj) {
4 if (obj == null || getClass() != obj.getClass()) return false;
5 Order other = (Order) obj;
6 return Objects.equals(orderId, other.orderId);
7}
8
9// Problem — if a subclass overrides nothing but equals uses getClass():
10// Order o1 = new Order("ORD-001");
11// Order o2 = new PriorityOrder("ORD-001"); // subclass
12// o1.equals(o2) → false — getClass() differs
13// o2.equals(o1) → false — getClass() differs
14// 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
| Topic | Link |
|---|---|
| How HashMap uses equals() and hashCode() to store and retrieve entries | Java HashMap → |
| How HashSet uses equals() and hashCode() for deduplication | Java HashSet → |
| How the Object class declares equals() and hashCode() as the root contract | Java Object Class → |
| How Collections framework builds on equals() and hashCode() throughout | Java Collections Framework → |
| How encapsulation protects the fields that equals() and hashCode() depend on | Java Encapsulation → |