Loading learning content...
There's a rule in object-oriented programming so important that violating it breaks half your application: If you override equals(), you must override hashCode(). This isn't a guideline or best practice—it's a contract violation that corrupts data structures.
This page teaches you how to implement both methods correctly, understand their deep relationship, and build objects that work reliably in collections. By the end, you'll write equals() and hashCode() with confidence, knowing exactly why each line of code exists.
By the end of this page, you will understand: (1) Why equals and hashCode must be overridden together, (2) The hashCode contract and its relationship to equals, (3) Step-by-step implementation patterns, (4) How hash codes work in collections, and (5) Strategies for generating quality hash codes.
To understand why equals() and hashCode() are inseparable, we need to understand how hash-based collections work.
The Bucket System:
A HashMap or HashSet doesn't store objects in a simple list. Instead, it uses buckets—an array of linked lists (or trees). When you add an object:
object.hashCode()index = hashCode % numberOfBucketsWhen you search for an object:
123456789101112131415161718192021222324
HashMap Internal Structure (simplified)═══════════════════════════════════════════════════════════════ Buckets (array)┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐│ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │└──┬──┴─────┴──┬──┴─────┴─────┴──┬──┴─────┴─────┘ │ │ │ ▼ ▼ ▼┌─────┐ ┌─────┐ ┌─────┐│ A=1 │ │ B=2 │ │ D=4 │└──┬──┘ └──┬──┘ └──┬──┘ │ │ │ ▼ ▼ ▼┌─────┐ ┌─────┐ ┌─────┐│ E=5 │ │ C=3 │ │ F=6 │└─────┘ └─────┘ └─────┘ Key insight: - hashCode() determines WHICH BUCKET to look in- equals() determines WHICH ITEM in the bucket matches If hashCode is wrong, you search the wrong bucketand NEVER find your item, even though it exists!The Catastrophic Failure:
If you override equals() but not hashCode(), two 'equal' objects can have different hash codes (inherited from Object.hashCode, which is based on memory address). This means:
new Person("Alice") to a HashSet (goes to bucket 3)contains(new Person("Alice")) (looks in bucket 7)false, even though an 'equal' person is in the set!The set is corrupted. Items you add can't be found. The equals() contract is satisfied, but the hashCode() contract is violated.
If two objects are equal according to equals(), they MUST have the same hashCode(). Violating this breaks HashMap, HashSet, Hashtable, and any other hash-based structure—silently, insidiously, and completely.
Just like equals(), hashCode() has a formal contract that must be satisfied. The contract has three parts:
| Requirement | Formal Statement | Practical Meaning |
|---|---|---|
| Consistency | Multiple calls return same value (if object unchanged) | hashCode doesn't randomly change |
| Equals → Same Hash | If a.equals(b), then a.hashCode() == b.hashCode() | Equal objects MUST have same hash |
| Unequal → Different Hash (optional) | If !a.equals(b), hash MAY differ | Ideally different for performance |
Critical Observations:
Same hash does NOT imply equals. Different objects can (and will) have the same hash code—this is called a collision. Only the reverse is required: equal objects must have the same hash.
The third rule is about performance, not correctness. Technically, you could return 0 for all objects and satisfy the contract. But then every object goes to bucket 0, and HashMap degrades to O(n) linear search. A good hash function distributes objects evenly across buckets.
1234567891011121314151617181920212223242526272829303132
// Understanding the contract through examples class Product { private String id; private String name; // Objects are equal if their IDs match @Override public boolean equals(Object o) { if (!(o instanceof Product)) return false; return id.equals(((Product) o).id); } // CORRECT: hashCode uses the same field(s) as equals @Override public int hashCode() { return id.hashCode(); }} // Verify the contract:Product p1 = new Product("ABC", "Widget");Product p2 = new Product("ABC", "Gadget"); // Same ID, different name p1.equals(p2); // true (same ID)p1.hashCode() == p2.hashCode(); // true! (same ID produces same hash)// Contract satisfied: equal objects have same hash Product p3 = new Product("XYZ", "Widget");p1.equals(p3); // false (different ID)p1.hashCode() == p3.hashCode(); // false (different ID, likely different hash)// Not required, but good: unequal objects have different hashesUse exactly the same fields in hashCode() as you use in equals(). If equals uses id and email, hashCode uses id and email. This guarantees the contract is satisfied and produces good distribution.
Let's build a complete, correct equals() method step by step. We'll use a BankAccount class as our example.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
public class BankAccount { private final String accountNumber; // Primary identifier private final String routingNumber; // Part of identity private double balance; // Mutable - NOT in equals private String nickname; // Optional metadata - NOT in equals @Override public boolean equals(Object obj) { //═══════════════════════════════════════════════════════════ // STEP 1: Identity check (this == obj) //═══════════════════════════════════════════════════════════ // If comparing to ourselves, immediately return true. // This satisfies reflexivity and is an optimization. if (this == obj) { return true; } //═══════════════════════════════════════════════════════════ // STEP 2: Null check //═══════════════════════════════════════════════════════════ // Nothing equals null. Return false. // (The getClass() check below also handles null, but // being explicit is clearer.) if (obj == null) { return false; } //═══════════════════════════════════════════════════════════ // STEP 3: Type check //═══════════════════════════════════════════════════════════ // Use getClass() for strict type matching. // A SavingsAccount should not equal a CheckingAccount // even if account numbers match. if (getClass() != obj.getClass()) { return false; } //═══════════════════════════════════════════════════════════ // STEP 4: Cast //═══════════════════════════════════════════════════════════ // Safe after type check. Now we can access fields. BankAccount other = (BankAccount) obj; //═══════════════════════════════════════════════════════════ // STEP 5: Field-by-field comparison //═══════════════════════════════════════════════════════════ // Compare only the fields that define "equality" for your domain. // Here: account + routing number identify an account. // Balance and nickname are NOT part of identity. return Objects.equals(accountNumber, other.accountNumber) && Objects.equals(routingNumber, other.routingNumber); // Note: Objects.equals() is null-safe // Objects.equals(null, null) returns true // Objects.equals("x", null) returns false without NPE }}Now let's implement a matching hashCode(). The key principle: use the same fields as equals().
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
public class BankAccount { private final String accountNumber; private final String routingNumber; private double balance; // NOT in hashCode (not in equals) private String nickname; // NOT in hashCode (not in equals) //═══════════════════════════════════════════════════════════════ // APPROACH 1: Objects.hash() - Simple and Clean //═══════════════════════════════════════════════════════════════ @Override public int hashCode() { // Objects.hash handles nulls and combines hashes automatically return Objects.hash(accountNumber, routingNumber); } //═══════════════════════════════════════════════════════════════ // APPROACH 2: Manual calculation - More control, slightly faster //═══════════════════════════════════════════════════════════════ @Override public int hashCode() { // Start with a non-zero prime int result = 17; // For each field, compute its hash and combine // Use 31 as multiplier (prime, allows compiler optimization) result = 31 * result + (accountNumber == null ? 0 : accountNumber.hashCode()); result = 31 * result + (routingNumber == null ? 0 : routingNumber.hashCode()); return result; } //═══════════════════════════════════════════════════════════════ // APPROACH 3: Cached hash code - For immutable objects //═══════════════════════════════════════════════════════════════ private int cachedHashCode; // 0 = not computed yet @Override public int hashCode() { int h = cachedHashCode; if (h == 0) { h = Objects.hash(accountNumber, routingNumber); cachedHashCode = h; } return h; } // WARNING: Only safe for immutable objects!}| Field Type | Hash Code Expression | Notes |
|---|---|---|
| boolean | value ? 1 : 0 | Simple true/false mapping |
| byte, char, short, int | (int) value | Direct cast to int |
| long | (int)(value ^ (value >>> 32)) | Mix both halves |
| float | Float.floatToIntBits(value) | Bit representation |
| double | Combine Double.doubleToLongBits(), then long→int | Two-step conversion |
| Object | value == null ? 0 : value.hashCode() | Delegate with null check |
| Array | Arrays.hashCode(array) | Element-by-element |
Objects.hash() handles all the complexity for you—null safety, type conversion, combining. The manual approach is only needed for (1) performance-critical code where varargs overhead matters, (2) arrays (Objects.hash doesn't deep-hash), or (3) learning purposes.
Let's visualize how equals() and hashCode() work together in a HashSet. Understanding this mechanism helps you debug collection issues and write correct implementations.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
ADDING AN ELEMENT: set.add(new Product("ABC", "Widget"))════════════════════════════════════════════════════════════ Step 1: Compute hash hashCode() → "ABC".hashCode() → 64578 Step 2: Compute bucket bucket = 64578 % 8 = 2 Step 3: Check bucket 2 for existing equal element Bucket 2: [empty] No equals() calls needed Step 4: Insert into bucket 2 Bucket 2: [Product(ABC, Widget)] ═══════════════════════════════════════════════════════════════ CHECKING MEMBERSHIP: set.contains(new Product("ABC", "Gadget"))═══════════════════════════════════════════════════════════════ Step 1: Compute hash of search key hashCode() → "ABC".hashCode() → 64578 (same as before!) Step 2: Compute bucket bucket = 64578 % 8 = 2 (same bucket!) Step 3: Search bucket 2 Found element: Product(ABC, Widget) Call: searchKey.equals(Product(ABC, Widget)) → "ABC".equals("ABC") → true! Step 4: Return result contains() returns TRUE Result: Found! Different object, but equal by ID, same hash → found. ════════════════════════════════════════════════════════════════ IF HASHCODE WAS WRONG: hashCode() returns random each time════════════════════════════════════════════════════════════════ add(new Product("ABC", "Widget")) hashCode() → 12345 Bucket: 12345 % 8 = 5 → stored in bucket 5 contains(new Product("ABC", "Widget")) hashCode() → 67890 (different!) Bucket: 67890 % 8 = 2 → looks in bucket 2 Bucket 2 is EMPTY Result: NOT FOUND, even though equal object exists in bucket 5!hashCode() quickly narrows down candidates by bucket. equals() does the precise comparison. If hashCode() sends you to the wrong bucket, equals() never gets a chance to find the match. This is why consistent hash codes for equal objects are mandatory.
A hash function is correct if equal objects have equal hashes. It's good if it distributes objects evenly across buckets. A bad hash function is correct but slow.
The Terrible Hash Function:
123456789101112131415161718192021222324252627282930313233343536373839404142
// CORRECT but TERRIBLE: All objects in one bucket@Overridepublic int hashCode() { return 42; // Every object has hash 42} // What happens with 1000 objects in a HashSet:// - All 1000 in bucket (42 % buckets)// - contains() must scan all 1000: O(n)// - NO benefit from hashing // ═══════════════════════════════════════════════════════════ // CORRECT and GOOD: Evenly distributed@Overridepublic int hashCode() { return Objects.hash(field1, field2, field3);} // With a good hash function:// - 1000 objects spread across ~1000 buckets// - contains() checks ~1 object: O(1)// - Full hashing benefit // ═══════════════════════════════════════════════════════════ // INCORRECT: Uses field not in equals@Overridepublic boolean equals(Object o) { if (!(o instanceof User)) return false; return id.equals(((User) o).id); // Only id} @Overridepublic int hashCode() { return Objects.hash(id, email); // WRONG: includes email!} // Two users with same ID but different emails:// - equals() returns true (same id)// - hashCode() returns different values (different email)// - CONTRACT VIOLATED: equal objects, different hashesThe manual hash function uses 31 as a multiplier because: (1) It's prime, reducing patterns, (2) 31 * i can be optimized to (i << 5) - i, which is fast, (3) It's been empirically shown to provide good distribution. This is the approach used in Java's String.hashCode().
Let's put everything together with a complete, production-quality implementation. This example includes all edge cases, proper null handling, and documentation.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
import java.util.Objects;import java.util.Arrays; /** * A product in an inventory system. * * Equality is based on SKU and warehouseId. * The same SKU in different warehouses represents different inventory items. * * Price and quantity are operational data, not identity. */public final class InventoryItem { // Identity fields (used in equals/hashCode) private final String sku; private final String warehouseId; // Operational fields (NOT used in equals/hashCode) private double price; private int quantity; // Array field example private final String[] tags; public InventoryItem(String sku, String warehouseId, double price, int quantity, String[] tags) { // Validate required identity fields this.sku = Objects.requireNonNull(sku, "SKU cannot be null"); this.warehouseId = Objects.requireNonNull(warehouseId, "Warehouse ID cannot be null"); this.price = price; this.quantity = quantity; // Defensive copy of mutable array this.tags = tags == null ? null : Arrays.copyOf(tags, tags.length); } @Override public boolean equals(Object obj) { // Step 1: Identity check if (this == obj) { return true; } // Step 2: Null and type check (combined for efficiency) if (obj == null || getClass() != obj.getClass()) { return false; } // Step 3: Cast InventoryItem other = (InventoryItem) obj; // Step 4: Compare identity fields only // Note: Using Objects.equals for null-safety (even though we // require non-null in constructor, defensive coding is good) return Objects.equals(sku, other.sku) && Objects.equals(warehouseId, other.warehouseId); // NOT comparing: price, quantity, tags // These can change without changing the item's identity } @Override public int hashCode() { // MUST use same fields as equals return Objects.hash(sku, warehouseId); } @Override public String toString() { return String.format("InventoryItem[sku=%s, warehouse=%s, " + "price=%.2f, qty=%d]", sku, warehouseId, price, quantity); } // Getters (setters only for mutable operational fields) public String getSku() { return sku; } public String getWarehouseId() { return warehouseId; } public double getPrice() { return price; } public int getQuantity() { return quantity; } public String[] getTags() { // Defensive copy on output return tags == null ? null : Arrays.copyOf(tags, tags.length); } public void setPrice(double price) { this.price = price; } public void setQuantity(int quantity) { this.quantity = quantity; }}1234567891011121314151617181920212223242526272829303132
// Demonstration of correct behaviorpublic class Demo { public static void main(String[] args) { // Create inventory items InventoryItem item1 = new InventoryItem("SKU-001", "WH-EAST", 29.99, 100, new String[]{"popular"}); InventoryItem item2 = new InventoryItem("SKU-001", "WH-EAST", 34.99, 50, // Different price/qty new String[]{"sale"}); // Different tags InventoryItem item3 = new InventoryItem("SKU-001", "WH-WEST", 29.99, 100, null); // Different warehouse // Same SKU+Warehouse = equal (regardless of price/qty/tags) System.out.println(item1.equals(item2)); // true System.out.println(item1.hashCode() == item2.hashCode()); // true // Different warehouse = not equal (even with same SKU) System.out.println(item1.equals(item3)); // false // Works correctly in collections Set<InventoryItem> inventory = new HashSet<>(); inventory.add(item1); System.out.println(inventory.contains(item2)); // true! Same identity System.out.println(inventory.contains(item3)); // false, different warehouse // Size remains 1 even though we added twice inventory.add(item2); // Doesn't add duplicate System.out.println(inventory.size()); // 1 }}We've covered the mechanics of implementing equals() and hashCode() correctly. Here's a consolidation of the key points and a practical checklist.
if (this == obj) return true; is fast and satisfies reflexivity.if (obj == null || getClass() != obj.getClass()) return false;What's Next:
We've covered identity, equality, and implementing equals/hashCode. The final page explores consistency requirements—the subtle rules about when and how objects can change, and how to prevent the silent data corruption that occurs when mutable objects are used as keys.
You now know how to implement equals() and hashCode() correctly. You understand the contract, the relationship between the methods, and the patterns that produce correct, efficient implementations. Next: Consistency Requirements.