Java equals() and hashCode()
Java equals() and hashCode()
Two methods on Object. Both inherited by every Java class. Both quietly broken in every class that never overrides them — and that brokenness only shows up at runtime, in the form of HashMap lookups that return null for keys that look identical, or HashSet that silently stores what should be duplicates.
Understanding equals() and hashCode() means understanding the contract between them, why that contract exists, and what breaks when it is violated.
What Are equals() and hashCode()?
Every Java class inherits two methods from java.lang.Object:
equals(Object obj) determines whether two object references point to logically equivalent objects. By default — when not overridden — it uses ==: identity comparison. Two different new Product("LAPTOP-001") objects are not equal by default even though they represent the same product.
hashCode() returns an int that represents the object for hashing purposes. By default, it is based on the object's memory address — a value that varies between JVM runs and has nothing to do with the object's logical content.
The diagram below shows what happens with and without overriding these methods.
WITHOUT OVERRIDE (inherits Object behaviour):
Product p1 = new Product("LAPTOP-001", 45999.0);
Product p2 = new Product("LAPTOP-001", 45999.0);
p1.equals(p2) → false (different memory addresses)
p1 == p2 → false (different references)
p1.hashCode() → 1829164700 (some address-based number)
p2.hashCode() → 1347067481 (a different address-based number)
WITH OVERRIDE (based on SKU field):
p1.equals(p2) → true (same SKU value)
p1.hashCode() → same (same SKU produces same hash)
HashMap result WITHOUT override:
map.put(p1, "in stock");
map.get(p2) → null (p2 seen as a different key — WRONG)
HashMap result WITH override:
map.put(p1, "in stock");
map.get(p2) → "in stock" (p2 recognized as same key — CORRECT)
This is the core problem both methods exist to solve: giving hash-based collections a way to recognise logically equal objects regardless of their memory location.
The Contract Between equals() and hashCode()
The Java specification defines a contract that any correct implementation must satisfy. Violating it breaks HashMap, HashSet, and every other hash-based collection silently — no exception is thrown, the data simply disappears or duplicates where it should not.
THE CONTRACT (from java.lang.Object documentation): Rule 1 — CONSISTENCY WITHIN A RUN: Multiple calls to hashCode() on the same object must return the same integer, as long as the fields used in equals() are not modified. Rule 2 — EQUAL OBJECTS MUST HAVE EQUAL HASH CODES: if a.equals(b) is true, then a.hashCode() == b.hashCode() MUST be true. This is the critical rule — breaking it makes HashMap lose entries. Rule 3 — UNEQUAL OBJECTS NEED NOT HAVE DIFFERENT HASH CODES: if a.hashCode() == b.hashCode(), a.equals(b) MAY be false. Collisions (same hash, different object) are allowed and handled. But many collisions degrade HashMap from O(1) to O(n). Rule 4 — SYMMETRY, REFLEXIVITY, TRANSITIVITY of equals(): a.equals(a) must be true (reflexive) a.equals(b) == b.equals(a) must be true (symmetric) a.equals(b) && b.equals(c) implies a.equals(c) (transitive) a.equals(null) must be false (null-safe)
Rule 2 is the one that causes real bugs. It says: if two objects are considered equal, they MUST produce the same hash code. The reverse — equal hash codes imply equal objects — is NOT required.
How HashMap Uses Both Methods
The internal working of HashMap makes the contract concrete. Every put() and get() calls both methods.
HashMap<Product, String> catalogue = new HashMap<>();
catalogue.put(product, "in stock");
catalogue.get(anotherProduct); ← how does this work?
STEP 1: Compute hash
int h = anotherProduct.hashCode();
int hash = h ^ (h >>> 16); ← spread high bits into low bits
STEP 2: Find bucket
int index = (capacity - 1) & hash; ← typically 0..15 for default size
STEP 3: Search bucket chain
For each Node in table[index]:
if (node.hash == hash && node.key.equals(anotherProduct)):
return node.value; ← found!
return null; ← not found
The two-step process:
hashCode() narrows the search to one bucket → fast (O(1))
equals() finds the exact match in the chain → precise
If hashCode() is overridden but equals() is not, two logically equal objects land in the same bucket but equals() returns false (default identity check), so no match is found. If equals() is overridden but hashCode() is not, two equal objects may land in completely different buckets and the get() returns null.
Basic Overview — equals() from Object
The default equals() and what overriding it means, starting from the simplest case.
1// File: EqualsBasicsDemo.java
2
3import java.util.Objects;
4
5public class EqualsBasicsDemo {
6
7 // Simple value class — two products are equal if their SKU matches
8 static class Product {
9 private final String sku;
10 private final String name;
11 private final double price;
12
13 Product(String sku, String name, double price) {
14 this.sku = sku;
15 this.name = name;
16 this.price = price;
17 }
18
19 // Without this override, equals() uses identity (==)
20 @Override
21 public boolean equals(Object obj) {
22 if (this == obj) return true; // same reference — trivially equal
23 if (obj == null) return false; // null is never equal to an object
24 if (!(obj instanceof Product other)) return false; // different class
25 return Objects.equals(this.sku, other.sku); // compare by SKU only
26 }
27
28 @Override
29 public String toString() { return "Product[" + sku + "]"; }
30 }
31
32 public static void main(String[] args) {
33
34 Product p1 = new Product("LAPTOP-001", "Lenovo ThinkPad", 45999.0);
35 Product p2 = new Product("LAPTOP-001", "Lenovo ThinkPad", 45999.0);
36 Product p3 = new Product("MOUSE-007", "Logitech MX", 1299.0);
37
38 System.out.println("=== Reference comparison (==) ===");
39 System.out.println("p1 == p2 : " + (p1 == p2)); // always false — different objects
40 System.out.println("p1 == p1 : " + (p1 == p1)); // always true — same reference
41
42 System.out.println("\n=== Logical comparison (equals) ===");
43 System.out.println("p1.equals(p2) : " + p1.equals(p2)); // true — same SKU
44 System.out.println("p1.equals(p3) : " + p1.equals(p3)); // false — different SKU
45 System.out.println("p1.equals(null): " + p1.equals(null)); // false — null safe
46
47 System.out.println("\n=== Symmetry check ===");
48 System.out.println("p1.equals(p2) : " + p1.equals(p2)); // true
49 System.out.println("p2.equals(p1) : " + p2.equals(p1)); // true — must match
50 }
51}Output:
=== Reference comparison (==) ===
p1 == p2 : false
p1 == p1 : true
=== Logical comparison (equals) ===
p1.equals(p2) : true
p1.equals(p3) : false
p1.equals(null): false
=== Symmetry check ===
p1.equals(p2) : true
p2.equals(p1) : true
Implementing hashCode() Correctly
hashCode() must be implemented alongside equals() — they form a pair. The standard modern approach uses Objects.hash(), which handles null fields safely and applies a good hash mix automatically.
1// File: HashCodeImplementationDemo.java
2
3import java.util.HashMap;
4import java.util.HashSet;
5import java.util.Map;
6import java.util.Objects;
7import java.util.Set;
8
9public class HashCodeImplementationDemo {
10
11 static class UserAccount {
12 private final String userId;
13 private final String email;
14 private final String role;
15
16 UserAccount(String userId, String email, String role) {
17 this.userId = userId;
18 this.email = email;
19 this.role = role;
20 }
21
22 @Override
23 public boolean equals(Object obj) {
24 if (this == obj) return true;
25 if (!(obj instanceof UserAccount other)) return false;
26 // Two accounts are equal if userId AND email both match
27 return Objects.equals(this.userId, other.userId)
28 && Objects.equals(this.email, other.email);
29 }
30
31 @Override
32 public int hashCode() {
33 // Objects.hash() combines fields using a prime-based mix
34 // Both fields used in equals() must appear here — no more, no less
35 return Objects.hash(userId, email);
36 }
37
38 @Override
39 public String toString() {
40 return "User[" + userId + ", " + email + ", " + role + "]";
41 }
42 }
43
44 public static void main(String[] args) {
45
46 UserAccount acc1 = new UserAccount("U001", "priya@zepto.in", "ADMIN");
47 UserAccount acc2 = new UserAccount("U001", "priya@zepto.in", "ADMIN"); // same logical user
48 UserAccount acc3 = new UserAccount("U002", "rohan@zepto.in", "USER");
49
50 System.out.println("=== hashCode values ===");
51 System.out.println("acc1.hashCode() : " + acc1.hashCode());
52 System.out.println("acc2.hashCode() : " + acc2.hashCode()); // must equal acc1's hash
53 System.out.println("acc3.hashCode() : " + acc3.hashCode()); // different — different user
54
55 System.out.println("\n=== HashSet — deduplication ===");
56 Set<UserAccount> activeUsers = new HashSet<>();
57 activeUsers.add(acc1);
58 activeUsers.add(acc2); // duplicate — add() returns false, set unchanged
59 activeUsers.add(acc3);
60 System.out.println("Set size: " + activeUsers.size()); // 2, not 3
61 activeUsers.forEach(u -> System.out.println(" " + u));
62
63 System.out.println("\n=== HashMap — key lookup ===");
64 Map<UserAccount, String> sessionMap = new HashMap<>();
65 sessionMap.put(acc1, "SESSION-TOKEN-XYZ");
66
67 // acc2 is a different object but equal to acc1 — must find the token
68 String token = sessionMap.get(acc2);
69 System.out.println("get(acc2) token : " + token); // SESSION-TOKEN-XYZ — not null
70 System.out.println("get(acc3) token : " + sessionMap.get(acc3)); // null — different user
71 }
72}Output:
=== hashCode values ===
acc1.hashCode() : 1073314563
acc2.hashCode() : 1073314563
acc3.hashCode() : 1073346271
=== HashSet — deduplication ===
Set size: 2
User[U001, priya@zepto.in, ADMIN]
User[U002, rohan@zepto.in, USER]
=== HashMap — key lookup ===
get(acc2) token : SESSION-TOKEN-XYZ
get(acc3) token : null
Three Ways to Implement equals() and hashCode()
Option 1 — Manual Implementation
Full control, most verbose. Useful when fine-grained control over comparison or performance is needed.
1// File: ManualImplementation.java
2
3import java.util.Objects;
4
5public class ManualImplementation {
6
7 static class OrderItem {
8 private final String productId;
9 private final int quantity;
10 private final double unitPrice;
11
12 OrderItem(String productId, int quantity, double unitPrice) {
13 this.productId = productId;
14 this.quantity = quantity;
15 this.unitPrice = unitPrice;
16 }
17
18 @Override
19 public boolean equals(Object obj) {
20 if (this == obj) return true;
21 if (obj == null || getClass() != obj.getClass()) return false;
22 OrderItem other = (OrderItem) obj;
23 return this.quantity == other.quantity
24 && Double.compare(this.unitPrice, other.unitPrice) == 0
25 && Objects.equals(this.productId, other.productId);
26 }
27
28 // Manual prime-based implementation — what IDE generators produce
29 @Override
30 public int hashCode() {
31 int result = Objects.hashCode(productId);
32 result = 31 * result + quantity;
33 result = 31 * result + Double.hashCode(unitPrice);
34 return result;
35 }
36 }
37}Option 2 — Objects.hash() (Recommended)
Cleaner, null-safe, and harder to get wrong. Use this in most production code.
1// File: ObjectsHashDemo.java
2
3import java.util.Objects;
4
5public class ObjectsHashDemo {
6
7 static class DeliveryAddress {
8 private final String street;
9 private final String city;
10 private final String pincode;
11
12 DeliveryAddress(String street, String city, String pincode) {
13 this.street = street;
14 this.city = city;
15 this.pincode = pincode;
16 }
17
18 @Override
19 public boolean equals(Object obj) {
20 if (this == obj) return true;
21 if (!(obj instanceof DeliveryAddress other)) return false;
22 return Objects.equals(street, other.street)
23 && Objects.equals(city, other.city)
24 && Objects.equals(pincode,other.pincode);
25 }
26
27 @Override
28 public int hashCode() {
29 // Single call — all fields combined, null-safe
30 return Objects.hash(street, city, pincode);
31 }
32 }
33
34 public static void main(String[] args) {
35
36 DeliveryAddress addr1 = new DeliveryAddress("42 MG Road", "Bengaluru", "560001");
37 DeliveryAddress addr2 = new DeliveryAddress("42 MG Road", "Bengaluru", "560001");
38 DeliveryAddress addr3 = new DeliveryAddress("15 Linking Rd", "Mumbai", "400054");
39
40 System.out.println("addr1.equals(addr2) : " + addr1.equals(addr2)); // true
41 System.out.println("addr1.hashCode() == addr2.hashCode(): "
42 + (addr1.hashCode() == addr2.hashCode())); // true
43 System.out.println("addr1.equals(addr3) : " + addr1.equals(addr3)); // false
44 }
45}Output:
addr1.equals(addr2) : true
addr1.hashCode() == addr2.hashCode(): true
addr1.equals(addr3) : false
Option 3 — Java Records (Java 16+)
Records auto-generate correct equals(), hashCode(), and toString() from all components. For immutable value classes, this is the cleanest approach.
1// File: RecordEqualsDemo.java
2
3import java.util.HashSet;
4import java.util.Set;
5
6public class RecordEqualsDemo {
7
8 // record generates equals, hashCode, toString automatically from components
9 record Coupon(String code, int discountPercent) {}
10
11 public static void main(String[] args) {
12
13 Coupon c1 = new Coupon("SAVE20", 20);
14 Coupon c2 = new Coupon("SAVE20", 20);
15 Coupon c3 = new Coupon("FLAT50", 50);
16
17 System.out.println("c1.equals(c2) : " + c1.equals(c2)); // true — auto-generated
18 System.out.println("c1.equals(c3) : " + c1.equals(c3)); // false
19
20 Set<Coupon> appliedCoupons = new HashSet<>();
21 appliedCoupons.add(c1);
22 appliedCoupons.add(c2); // duplicate — rejected by HashSet
23 appliedCoupons.add(c3);
24 System.out.println("Applied coupons count: " + appliedCoupons.size()); // 2
25 }
26}Output:
c1.equals(c2) : true
c1.equals(c3) : false
Applied coupons count: 2
Real-World Example — Flipkart Wish List Deduplication
A product wish list should hold unique items — adding the same product twice must be a no-op. If equals() and hashCode() are not correctly implemented on the WishlistItem class, HashSet stores both entries as distinct items. The bug is invisible until customers notice that their wish list shows duplicates, or until the checkout logic double-counts quantities.
1// File: WishlistItem.java
2
3import java.util.Objects;
4
5public class WishlistItem {
6
7 private final String productId; // the identity of the item
8 private final String productName;
9 private final double price;
10 private int quantity;
11
12 public WishlistItem(String productId, String productName,
13 double price, int quantity) {
14 this.productId = productId;
15 this.productName = productName;
16 this.price = price;
17 this.quantity = quantity;
18 }
19
20 public String getProductId() { return productId; }
21 public String getProductName() { return productName; }
22 public double getPrice() { return price; }
23 public int getQuantity() { return quantity; }
24 public void setQuantity(int qty) { this.quantity = qty; }
25
26 // Two wish list items are the same product if productId matches
27 // price and quantity are NOT part of identity — a price change does
28 // not make it a different product
29 @Override
30 public boolean equals(Object obj) {
31 if (this == obj) return true;
32 if (!(obj instanceof WishlistItem other)) return false;
33 return Objects.equals(this.productId, other.productId);
34 }
35
36 @Override
37 public int hashCode() {
38 return Objects.hash(productId);
39 }
40
41 @Override
42 public String toString() {
43 return String.format("%-20s x%-2d Rs.%.2f [%s]",
44 productName, quantity, price * quantity, productId);
45 }
46}1// File: WishlistService.java
2
3import java.util.HashMap;
4import java.util.Map;
5
6public class WishlistService {
7
8 // HashMap keyed by WishlistItem — equals/hashCode drive correct key lookup
9 private final Map<WishlistItem, WishlistItem> items = new HashMap<>();
10
11 public void addItem(WishlistItem newItem) {
12 if (items.containsKey(newItem)) {
13 // Product already in wish list — increase quantity instead of adding duplicate
14 WishlistItem existing = items.get(newItem);
15 existing.setQuantity(existing.getQuantity() + newItem.getQuantity());
16 System.out.println(" Updated qty for: " + existing.getProductName());
17 } else {
18 items.put(newItem, newItem);
19 System.out.println(" Added: " + newItem.getProductName());
20 }
21 }
22
23 public boolean removeItem(String productId) {
24 // Lookup by productId — works because equals uses only productId
25 WishlistItem probe = new WishlistItem(productId, "", 0, 0);
26 return items.remove(probe) != null;
27 }
28
29 public double totalValue() {
30 return items.values().stream()
31 .mapToDouble(item -> item.getPrice() * item.getQuantity())
32 .sum();
33 }
34
35 public void printWishlist() {
36 System.out.println("=".repeat(52));
37 System.out.println(" FLIPKART WISH LIST");
38 System.out.println("=".repeat(52));
39 if (items.isEmpty()) {
40 System.out.println(" (empty)");
41 } else {
42 items.values().forEach(item -> System.out.println(" " + item));
43 System.out.println("-".repeat(52));
44 System.out.printf(" Total value: Rs.%.2f%n", totalValue());
45 }
46 System.out.println("=".repeat(52));
47 }
48
49 public static void main(String[] args) {
50
51 WishlistService wishlist = new WishlistService();
52
53 wishlist.addItem(new WishlistItem("P001", "Logitech MX Keys", 5999.0, 1));
54 wishlist.addItem(new WishlistItem("P002", "Dell 24\" Monitor", 18999.0, 1));
55 wishlist.addItem(new WishlistItem("P003", "USB-C Hub", 2499.0, 1));
56
57 System.out.println("\nAfter initial adds:");
58 wishlist.printWishlist();
59
60 // Add P001 again — should update quantity, not create a duplicate entry
61 System.out.println("\nAdding Logitech MX Keys again (duplicate product):");
62 wishlist.addItem(new WishlistItem("P001", "Logitech MX Keys", 5999.0, 1));
63
64 System.out.println("\nAfter duplicate add:");
65 wishlist.printWishlist();
66
67 // Remove by productId — creates a probe object; equals matches on productId
68 System.out.println("\nRemoving USB-C Hub:");
69 wishlist.removeItem("P003");
70 wishlist.printWishlist();
71 }
72}Output:
Added: Logitech MX Keys
Added: Dell 24" Monitor
Added: USB-C Hub
After initial adds:
====================================================
FLIPKART WISH LIST
====================================================
Logitech MX Keys x1 Rs.5999.00 [P001]
Dell 24" Monitor x1 Rs.18999.00 [P002]
USB-C Hub x1 Rs.2499.00 [P003]
----------------------------------------------------
Total value: Rs.27497.00
====================================================
Adding Logitech MX Keys again (duplicate product):
Updated qty for: Logitech MX Keys
After duplicate add:
====================================================
FLIPKART WISH LIST
====================================================
Logitech MX Keys x2 Rs.11998.00 [P001]
Dell 24" Monitor x1 Rs.18999.00 [P002]
USB-C Hub x1 Rs.2499.00 [P003]
----------------------------------------------------
Total value: Rs.33496.00
====================================================
Removing USB-C Hub:
====================================================
FLIPKART WISH LIST
====================================================
Logitech MX Keys x2 Rs.11998.00 [P001]
Dell 24" Monitor x1 Rs.18999.00 [P002]
----------------------------------------------------
Total value: Rs.30997.00
====================================================
The removeItem() method creates a probe WishlistItem with only the productId field and calls map.remove(probe). It works because equals() compares only productId — the rest of the fields can be empty. This is a production pattern that only works when the equals contract is implemented correctly.
Performance Considerations
A poor hashCode() implementation degrades HashMap from O(1) to O(n). The examples below show the spectrum from worst to best.
HASH CODE QUALITY — IMPACT ON HASHMAP:
CONSTANT hash (absolute worst):
@Override public int hashCode() { return 42; }
Every entry lands in bucket 42 — HashMap becomes a linked list
get() degrades from O(1) average to O(n) — fatal for large maps
PARTIAL fields (common mistake):
@Override public int hashCode() { return Objects.hash(city); }
All products from Mumbai land in the same bucket
Cities in India concentrate entries — poor distribution
ALL identity fields (correct):
@Override public int hashCode() { return Objects.hash(productId, sku); }
Entries spread across all buckets
get() stays O(1) average — maximum performance
JAVA 8 TREEIFICATION (safety net):
When any bucket chain grows beyond 8 entries, HashMap converts
the chain to a Red-Black tree — O(log n) instead of O(n)
This catches poor hashCode() implementations but does not fix them
Memory: Each HashMap node holds hash, key, value, and next pointer — approximately 48 bytes. A poor hash function that concentrates all entries in one chain wastes this structure without gaining any lookup benefit.
Best Practices
Always override both methods together, never just one. An IDE like IntelliJ or Eclipse generates both with one action — Alt+Insert → Generate → equals() and hashCode(). Overriding only equals() breaks HashSet and HashMap silently. Overriding only hashCode() breaks equals()-based comparisons in List.contains() and Collections.frequency().
Include exactly the fields that define identity in both methods. If two Order objects with the same orderId are the same order, use only orderId in both equals() and hashCode(). Do not include mutable status fields — if the status changes after an Order is put in a HashSet, the hash code changes and the entry becomes unreachable.
Use Objects.hash() for hashCode() and Objects.equals() for field comparison in equals(). Both handle null fields correctly. Writing this.field.hashCode() throws NullPointerException if the field is null. Objects.hash(field) does not.
For value classes that will never change, use Java records. Records generate correct equals(), hashCode(), and toString() from all components automatically. They enforce immutability, which removes the mutable-key bug entirely. Any class that is logically a bundle of final values is a candidate for a record.
Common Mistakes
Mistake 1 — Overriding equals() Without hashCode()
1// WRONG — equals() overridden, hashCode() left as Object's identity hash
2class Employee {
3 String employeeId;
4
5 @Override
6 public boolean equals(Object obj) {
7 if (!(obj instanceof Employee other)) return false;
8 return Objects.equals(this.employeeId, other.employeeId);
9 }
10 // hashCode() NOT overridden — defaults to identity hash
11}
12
13// Consequence: two equal employees have different hash codes
14Employee e1 = new Employee("E-001");
15Employee e2 = new Employee("E-001");
16e1.equals(e2); // true — overridden equals says equal
17e1.hashCode() == e2.hashCode(); // false — different addresses!
18
19Set<Employee> team = new HashSet<>();
20team.add(e1);
21team.contains(e2); // false — e2 goes to different bucket than e1
22// The set silently stores both "E-001" employees as separate entriesMistake 2 — Using a Mutable Field in hashCode()
1// WRONG — status is mutable; changing it after insertion loses the entry
2class Task {
3 String taskId;
4 String status; // mutable — changes from OPEN to DONE
5
6 @Override public boolean equals(Object obj) { ... }
7
8 @Override
9 public int hashCode() {
10 return Objects.hash(taskId, status); // status changes after put()!
11 }
12}
13
14Set<Task> openTasks = new HashSet<>();
15Task task = new Task("T-001", "OPEN");
16openTasks.add(task);
17
18task.status = "DONE"; // hash code changes!
19
20openTasks.contains(task); // false — task is lost in wrong bucket
21openTasks.remove(task); // false — cannot remove either
22
23// CORRECT — hashCode uses only the immutable identity field
24@Override
25public int hashCode() {
26 return Objects.hash(taskId); // taskId never changes
27}Mistake 3 — Breaking Symmetry in equals()
1// WRONG — equals is not symmetric: employee.equals(manager) != manager.equals(employee)
2class Employee {
3 String id;
4 @Override
5 public boolean equals(Object obj) {
6 if (!(obj instanceof Employee)) return false; // accepts Manager as well
7 return Objects.equals(this.id, ((Employee) obj).id);
8 }
9}
10
11class Manager extends Employee {
12 @Override
13 public boolean equals(Object obj) {
14 if (!(obj instanceof Manager)) return false; // only accepts Manager
15 return Objects.equals(this.id, ((Manager) obj).id);
16 }
17}
18
19Employee emp = new Employee(); emp.id = "E-001";
20Manager mgr = new Manager(); mgr.id = "E-001";
21
22emp.equals(mgr); // true — Employee.equals accepts Manager
23mgr.equals(emp); // false — Manager.equals rejects Employee
24// Symmetry broken — this causes unpredictable List.contains() resultsMistake 4 — Comparing Floating-Point With ==
1// WRONG — floating-point equality with == misses precision edge cases
2@Override
3public boolean equals(Object obj) {
4 if (!(obj instanceof PricePoint other)) return false;
5 return this.amount == other.amount; // floating-point == unreliable
6}
7
8// CORRECT — use Double.compare() for doubles and BigDecimal.compareTo() for currency
9@Override
10public boolean equals(Object obj) {
11 if (!(obj instanceof PricePoint other)) return false;
12 return Double.compare(this.amount, other.amount) == 0;
13}Interview Questions
Q1. What is the contract between equals() and hashCode() in Java?
The contract has two key rules. First, if a.equals(b) returns true, then a.hashCode() must equal b.hashCode() — no exceptions. Second, the reverse is not required: equal hash codes do not imply equal objects — this is a hash collision and is handled by the chain or tree structure inside HashMap. Violating Rule 1 is the silent killer: HashMap stores an entry using key A's bucket (based on A's hash), but retrieval for an equal key B goes to a different bucket (B's different hash) and returns null. No exception fires — the data just vanishes.
Q2. What happens if you override equals() but not hashCode()?
HashSet silently stores duplicates and HashMap loses entries. When you put(key, value), the key's hash (from Object.hashCode()) determines the bucket. When you later get(equalKey), equalKey.hashCode() returns a different number (identity-based), so it looks in a different bucket, finds nothing, and returns null. list.contains() still works because ArrayList uses equals() for a linear scan — but every hash-based collection breaks. This is why the Java specification says both must always be overridden together.
Q3. Why should Map keys be immutable?
HashMap places an entry in a bucket based on the key's hashCode() at the time of put(). If the key is a mutable object and a field used in hashCode() changes after insertion, the key's hash code changes — but the entry stays in the original bucket. Any subsequent get() with the same (now-modified) key computes a new bucket, finds nothing there, and returns null. The entry is now unreachable — effectively lost. Using String, Integer, or a record as a map key avoids this entirely because those types are immutable.
Q4. What does Objects.hash() do internally?
Objects.hash(field1, field2, ...) calls Arrays.hashCode(new Object[]{field1, field2, ...}) internally. It iterates the array, starting with result = 1, and for each element computes result = 31 * result + (element == null ? 0 : element.hashCode()). The prime 31 is chosen because it produces good distribution for typical string-like values and is fast to compute as a bit-shift operation (n << 5) - n. Null fields are mapped to 0, making the method null-safe — calling field.hashCode() directly on a null field would throw NullPointerException.
Q5. How do Java records handle equals() and hashCode()?
Records automatically generate equals(), hashCode(), and toString() from all declared components (the fields in the record header). The generated equals() checks that both objects are of the same record type and all component fields are equal. The generated hashCode() uses all component fields. Records are immutable by default — all components are private final — which eliminates the mutable-key problem entirely. For value-class use cases in modern Java, records are the cleanest and least error-prone implementation choice.
Q6. When should you NOT include a field in equals() and hashCode()?
Exclude fields that are: (1) mutable and change after object creation — changing them after the object is placed in a HashSet or used as a HashMap key corrupts the collection; (2) derived or computed from other included fields — redundant inclusion does not add correctness but increases collision risk if the computation creates similar values; (3) not part of the object's identity — for example, a createdAt timestamp on a Product is metadata, not identity. Only include fields that you would compare to decide "are these the same thing in the domain model?"
FAQs
Do I need to override equals() and hashCode() for every Java class?
No — only for classes whose instances will be: used as HashMap or LinkedHashMap keys, stored in a HashSet or LinkedHashSet, compared with assertEquals() in unit tests by value, or passed to List.contains() or Collections.frequency() where content equality matters. For service classes, controller classes, or any class used by reference only, the default identity-based implementations are correct and safe.
What is the difference between == and equals() in Java?
== is the reference equality operator — it returns true only when both sides point to the exact same object in memory. equals() is a method that can be overridden to define logical equality. For String, Integer, and Long, equals() compares content. For classes that do not override equals(), it falls back to == behaviour. This is why new String("hello") == new String("hello") is false but .equals() between them is true.
Can two objects be equal but have different class types?
Technically yes — you can write an equals() that returns true for objects of different classes. But this almost always breaks the symmetry contract. If a.equals(b) returns true when b is of a parent or sibling class but b.equals(a) returns false because b's equals() uses instanceof more narrowly, symmetry is violated. The safest approach for inheritance hierarchies is to use getClass() != obj.getClass() instead of instanceof — this rejects cross-type equality entirely and keeps the contract clean.
What is the hashCode() value for a null key in HashMap?
HashMap specifically handles null keys by placing them in bucket 0 (index 0), bypassing the normal hashCode() call. This is why HashMap allows exactly one null key while ConcurrentHashMap and Hashtable do not — those implementations call hashCode() on the key, which throws NullPointerException for null.
Does String already have a good equals() and hashCode()?
Yes. String.equals() compares character content. String.hashCode() uses a deterministic formula: s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1], where s[i] is the character at position i. Crucially, String caches the hash code after the first call — subsequent hashCode() calls on the same String return the cached value in O(1), not O(n). This makes String an excellent HashMap key for performance.
What happens if hashCode() returns 0 for all objects?
The map still works correctly — all entries land in bucket 0, which becomes a single enormous chain (or tree after 8 entries). get() degrades from O(1) to O(n) for every lookup because the bucket chain must be traversed fully. A HashMap with 10,000 entries and a constant hashCode() performs like a linked list on every read. No data is lost — correctness is maintained — but performance collapses entirely.
Summary
equals() and hashCode() form a two-part contract that powers every hash-based collection in Java. The critical rule: equal objects must have equal hash codes — always, without exception. Violating this makes HashMap silently lose entries and HashSet silently accept duplicates, with no exception to alert you.
Override both methods together using the same fields. Use Objects.hash() for hashCode() and Objects.equals() for field comparison in equals() — both handle null safely. Only include fields that define the object's identity, and never include mutable fields that change after the object enters a collection.
For interviews: know the contract by heart, know what breaks when hashCode() is overridden without equals() and vice versa, explain why map keys must be immutable, and describe how HashMap uses both methods in its two-step lookup. These questions appear consistently from service-based campus placements to senior product-company rounds, and getting them right shows that you understand how Java collections actually work internally.
What to Read Next
| Topic | Link |
|---|---|
| How HashMap uses equals and hashCode internally for O(1) key lookup | Java HashMap → |
| How HashSet uses the same mechanism to enforce element uniqueness | Java HashSet → |
| How the Collections Framework relies on the equals-hashCode contract | Java Collections Framework → |
| What makes a class truly immutable and why immutable keys are safer | Java Immutable Class → |
| How Java records auto-generate correct equals, hashCode, and toString | Java Record Classes → |