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.