The equals() and hashCode() methods in Java have a crucial contract that must be followed for proper functioning in hash-based collections like HashMap, HashSet, and Hashtable.
The Contract Rules
- Consistency: If two objects are equal according to
equals(), they must have the samehashCode()value - Non-requirement: If two objects have the same
hashCode(), they are not necessarily equal (hash collisions are allowed) - Multiple calls:
hashCode()must consistently return the same integer when called multiple times on the same object (unless the object is modified) - Equals reflexivity:
x.equals(x)must always returntrue - Equals symmetry: If
x.equals(y)returnstrue, theny.equals(x)must also returntrue - Equals transitivity: If
x.equals(y)andy.equals(z)returntrue, thenx.equals(z)must returntrue - Equals consistency: Multiple invocations of
x.equals(y)must consistently return the same value - Null comparison:
x.equals(null)must returnfalse
Default Implementation
By default, Java provides implementations in the Object class:
public class Object { public boolean equals(Object obj) { return (this == obj); } public native int hashCode(); } The default equals() uses reference equality (==), and hashCode() typically returns the memory address of the object.
Why the Contract Matters
When using hash-based collections:
- First,
hashCode()is called to find the bucket - Then,
equals()is called to verify exact equality within that bucket
Map<Employee, String> map = new HashMap<>(); Employee emp1 = new Employee("John", 30); Employee emp2 = new Employee("John", 30); map.put(emp1, "Developer"); // Without proper hashCode/equals, this may return null String position = map.get(emp2); Proper Implementation Examples
Basic Implementation
public class Employee { private String name; private int age; private String department; public Employee(String name, int age, String department) { this.name = name; this.age = age; this.department = department; } @Override public boolean equals(Object o) { // 1. Check if same reference if (this == o) return true; // 2. Check if null or different class if (o == null || getClass() != o.getClass()) return false; // 3. Cast and compare significant fields Employee employee = (Employee) o; return age == employee.age && Objects.equals(name, employee.name) && Objects.equals(department, employee.department); } @Override public int hashCode() { return Objects.hash(name, age, department); } } Using Java 7+ Objects Utility Class
@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Employee employee = (Employee) o; return Objects.equals(name, employee.name) && age == employee.age && Objects.equals(department, employee.department); } @Override public int hashCode() { return Objects.hash(name, age, department); } Manual Implementation (for performance)
@Override public int hashCode() { int result = name != null ? name.hashCode() : 0; result = 31 * result + age; result = 31 * result + (department != null ? department.hashCode() : 0); return result; } Common Pitfalls and Best Practices
1. Inconsistent Fields
// BAD - Using non-final fields in hashCode() private int age; @Override public int hashCode() { return Objects.hash(name, age); // Age can change! } // GOOD - Use immutable fields or be aware of consequences 2. Missing Null Checks
// BAD - Potential NullPointerException @Override public int hashCode() { return name.hashCode() + age; // name could be null } // GOOD - Proper null handling @Override public int hashCode() { return Objects.hash(name, age); } 3. Inheritance Issues
public class Manager extends Employee { private int teamSize; // Must override both methods in subclass @Override public boolean equals(Object o) { if (!super.equals(o)) return false; if (!(o instanceof Manager)) return false; Manager manager = (Manager) o; return teamSize == manager.teamSize; } @Override public int hashCode() { return Objects.hash(super.hashCode(), teamSize); } } 4. Performance Considerations
// For frequently called objects, cache hashCode if immutable private int cachedHashCode; @Override public int hashCode() { if (cachedHashCode == 0) { cachedHashCode = Objects.hash(name, age, department); } return cachedHashCode; } Testing the Contract
public class EmployeeTest { @Test public void testEqualsHashCodeContract() { Employee emp1 = new Employee("John", 30, "Engineering"); Employee emp2 = new Employee("John", 30, "Engineering"); Employee emp3 = new Employee("Jane", 25, "Marketing"); // Reflexivity assertTrue(emp1.equals(emp1)); // Symmetry assertTrue(emp1.equals(emp2)); assertTrue(emp2.equals(emp1)); // HashCode consistency assertEquals(emp1.hashCode(), emp1.hashCode()); // Main contract: if equals, then same hashCode assertTrue(emp1.equals(emp2)); assertEquals(emp1.hashCode(), emp2.hashCode()); // But same hashCode doesn't guarantee equality // (hash collisions are possible but unlikely with good implementation) } } When to Override
Override equals() and hashCode() when:
- Using objects as keys in hash-based collections
- Storing objects in
HashSetor similar collections - You need value-based equality instead of reference equality
Key Takeaways
- Always override both methods together
- Use the same fields in both methods
- Follow all contract rules strictly
- Consider using
Objects.equals()andObjects.hash()for simplicity - Be careful with inheritance
- Test your implementation thoroughly
Failure to follow the contract can lead to subtle bugs that are hard to detect, especially in hash-based collections where objects might "disappear" or not be found properly.