Contents
- The equals–hashCode Contract
- Default Behavior (Object class)
- Overriding equals()
- Overriding hashCode()
- How HashMap Uses Both Methods
- Common Pitfalls
Java's contract (documented in Object) mandates:
- If a.equals(b) is true, then a.hashCode() == b.hashCode() must also be true.
- If a.hashCode() == b.hashCode(), it does not mean a.equals(b) (hash collisions are allowed).
- equals() must be reflexive, symmetric, transitive, consistent, and handle null gracefully.
Violating the contract causes silent bugs: objects that are "equal" may not be found in a HashMap or may appear as duplicates in a HashSet.
By default, Object.equals() performs reference equality — two references are equal only if they point to the same object in memory. Object.hashCode() returns a value derived from the object's memory address.
String a = new String("hello");
String b = new String("hello");
System.out.println(a == b); // false (different objects)
System.out.println(a.equals(b)); // true (String overrides equals)
System.out.println(a.hashCode() == b.hashCode()); // true (String overrides hashCode)
String, Integer, and other wrapper classes already override both methods correctly. For custom classes, you must override them yourself.
Follow this canonical pattern:
public class Employee {
private int id;
private String name;
// Constructor, getters omitted for brevity
@Override
public boolean equals(Object obj) {
if (this == obj) return true; // same reference
if (obj == null) return false; // null check
if (getClass() != obj.getClass()) return false; // type check
Employee other = (Employee) obj;
return this.id == other.id
&& Objects.equals(this.name, other.name);
}
}
Key steps:
- Return true immediately if this == obj.
- Return false for null or different class.
- Cast and compare each significant field. Use Objects.equals() for nullable fields to avoid NPE.
A good hash code should distribute objects uniformly. The easiest approach is Objects.hash():
@Override
public int hashCode() {
return Objects.hash(id, name); // combine significant fields
}
Manual implementation using the prime-multiplier pattern:
@Override
public int hashCode() {
int result = 17; // non-zero constant
result = 31 * result + id;
result = 31 * result + (name != null ? name.hashCode() : 0);
return result;
}
Rule of thumb: Include the same fields in hashCode() that you use in equals(). Never include mutable fields that may change after the object is placed in a HashMap.
When you call map.put(key, value):
- HashMap calls key.hashCode() to determine the bucket index.
- Inside the bucket, it calls key.equals(existingKey) to find a match or insert a new entry.
Map<Employee, String> map = new HashMap<>();
Employee e1 = new Employee(1, "Alice");
Employee e2 = new Employee(1, "Alice"); // logically same as e1
map.put(e1, "Engineering");
// Without overriding equals/hashCode: e2 lookup FAILS (different hash)
// With correct overrides: e2 lookup SUCCEEDS
System.out.println(map.get(e2)); // "Engineering" — if overrides are correct
If hashCode() is not overridden, e1 and e2 land in different buckets and the get() call returns null.
- Overriding only equals — objects are logically equal but have different hash codes; they won't be found in collections.
- Mutable key fields — mutating a field used in hashCode() after inserting into a HashMap changes its bucket; the entry becomes unreachable.
- Using instanceof in equals — can break symmetry in class hierarchies; prefer getClass() unless designing for polymorphism.
- Not calling super.hashCode() in inheritance — only do so if the parent's fields should also be part of equality.
Modern IDEs (IntelliJ, Eclipse) and libraries like Lombok (@EqualsAndHashCode) can auto-generate correct implementations. Java 16+ Records automatically generate correct equals/hashCode based on all component fields.