Java Tutorial
🔍

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.

Java
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.

Java
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.

Java
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.

Java
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.

Java
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.

Java
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}
Java
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()

Java
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 entries

Mistake 2 — Using a Mutable Field in hashCode()

Java
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()

Java
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() results

Mistake 4 — Comparing Floating-Point With ==

Java
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

TopicLink
How HashMap uses equals and hashCode internally for O(1) key lookupJava HashMap →
How HashSet uses the same mechanism to enforce element uniquenessJava HashSet →
How the Collections Framework relies on the equals-hashCode contractJava Collections Framework →
What makes a class truly immutable and why immutable keys are saferJava Immutable Class →
How Java records auto-generate correct equals, hashCode, and toStringJava Record Classes →
Java equals() and hashCode() | DevStackFlow