equals() and hashCode() Contract in Java

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

  1. Consistency: If two objects are equal according to equals(), they must have the same hashCode() value
  2. Non-requirement: If two objects have the same hashCode(), they are not necessarily equal (hash collisions are allowed)
  3. Multiple calls: hashCode() must consistently return the same integer when called multiple times on the same object (unless the object is modified)
  4. Equals reflexivity: x.equals(x) must always return true
  5. Equals symmetry: If x.equals(y) returns true, then y.equals(x) must also return true
  6. Equals transitivity: If x.equals(y) and y.equals(z) return true, then x.equals(z) must return true
  7. Equals consistency: Multiple invocations of x.equals(y) must consistently return the same value
  8. Null comparison: x.equals(null) must return false

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 HashSet or similar collections
  • You need value-based equality instead of reference equality

Key Takeaways

  1. Always override both methods together
  2. Use the same fields in both methods
  3. Follow all contract rules strictly
  4. Consider using Objects.equals() and Objects.hash() for simplicity
  5. Be careful with inheritance
  6. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper