The Default Behavior of equals()
In java.lang.Object, the base implementation simply checks reference identity:
public boolean equals(Object obj) {
return (this == obj);
}
Without overriding, two distinct instances are never equal, even if they hold identical field values.
A typical custom implementation compares logical attributes:
class Person {
private int id;
private String fullName;
public Person(int id, String fullName) {
this.id = id;
this.fullName = fullName;
}
@Override
public boolean equals(Object other) {
if (this == other) return true;
if (!(other instanceof Person)) return false;
Person that = (Person) other;
return this.id == that.id && Objects.equals(this.fullName, that.fullName);
}
}
Here, two Person objects are considered equal when their id and fullName match.
The Default Behavior of hashCode()
The default hashCode() in Object typically derives a value from the object's memory address. Each instance therefore gets a different hash code, regardless of logical equality.
A well-behaved override computes a hash from the same fields used in equals():
@Override
public int hashCode() {
return Objects.hash(id, fullName);
}
Key Reasons to Always Override Both
Contract Consistency
equals() and hashCode() share a critical rule: if a.equals(b) is true, then a.hashCode() == b.hashCode() must also be true. Failing to honor this breaks any hash-based collection.
Correct Behavior in Hash-Based Structures
HashMap, HashSet, Hashtable and similar containers rely on hash codes to distribute entries into buckets. The lookup process first locates the bucket via hashCode(), then uses equals() to find the exact key. When two equal objects produce different hash codes, a get() call may look in the wrong bucket and return null even though the key is logically present.
Performance Through Collision Reduction
A good hash function spreads entries evenly, minimizing the number of times equals() needs to be invoked during a collision.
Illustrative Failure Scenario
Consider the Person class with equals() overridden but hashCode() left unchanged:
class Person {
private int id;
private String fullName;
public Person(int id, String fullName) {
this.id = id;
this.fullName = fullName;
}
@Override
public boolean equals(Object other) {
if (this == other) return true;
if (!(other instanceof Person)) return false;
Person that = (Person) other;
return this.id == that.id && Objects.equals(this.fullName, that.fullName);
}
// hashCode() not overridden — uses Object's memory-address-based hash
}
Now populate a map and attempt retrieval:
public static void main(String[] args) {
Person p1 = new Person(101, "Alice");
Person p2 = new Person(101, "Alice");
Map<Person, String> directory = new HashMap<>();
directory.put(p1, "Engineering");
System.out.println("p1 equals p2? " + p1.equals(p2)); // true
System.out.println("p1 hash: " + p1.hashCode());
System.out.println("p2 hash: " + p2.hashCode());
System.out.println("Lookup p2: " + directory.get(p2)); // null!
}
Despite p1.equals(p2) returning true, directory.get(p2) returns null. The map stored p1 in one bucket but tries to find p2 in another, because their hash codes differ.
Once hashCode() is properly overridden:
@Override
public int hashCode() {
return Objects.hash(id, fullName);
}
p1 and p2 produce the same hash, the lookup succeeeds, and the contract is preserved.