Why Overriding hashCode and equals Is Critical: Avoid Memory Leaks and Bucket Chaos in Java
This article explains the contract between hashCode() and equals() in Java, shows the problems caused by violating it—such as incorrect behavior in HashMap/HashSet and memory leaks—and provides practical guidelines and code examples for implementing robust hashCode methods, including the use of Apache's HashCodeBuilder.
1. Overriding hashCode() and equals() Contract
Every Java object has two important methods, hashCode() and equals(), which should be overridden according to a well‑defined contract when objects are used as keys in collections like HashMap or HashSet. This article describes why and how to correctly override hashCode().
1.1 hashCode Contract
If two objects are equal, their hashCode() methods must return the same hash value.
Two questions arise: what happens if equal objects return different hash codes, and what if unequal objects return the same hash code?
Equal objects with different hashCode() values.
Unequal objects with identical hashCode() values.
1.1.1 Equal objects but different hashCode
If you store such objects in a HashSet or HashMap, the collection may behave unexpectedly because it relies on the hash code to locate the bucket where the object is stored.
In a HashMap, the put() operation stores the entry in a bucket determined by the object's hash code. When contains() or get() is called, the map recomputes the hash code, looks in the corresponding bucket, and compares the key with existing entries using equals(). If the hash code has changed, the lookup fails.
1.1.2 Unequal objects but same hashCode
The contract does not require distinct objects to have different hash codes, so collisions are allowed. However, many collisions degrade the performance of hash‑based collections.
1.2 Why Buckets?
Without buckets, a collection would have to scan every element to find a match. Buckets allow the collection to examine only the small subset of entries that share the same hash code, improving efficiency.
1.3 Implementing hashCode()
Writing a good hashCode() method is tricky. A naïve implementation that always returns a constant (e.g., return 1;) satisfies the contract but forces all entries into a single bucket, destroying performance.
1.3.1 Returning a Constant Value
@Override
public int hashCode() {
return 1;
}This approach is legal but results in poor performance because every object ends up in the same bucket.
1.3.2 Effective Java Guidelines
Joshua Bloch recommends the following pattern (from *Effective Java*):
Start with a non‑zero constant, e.g., int result = 17;.
For each significant field f, compute an int c and combine it: result = 37 * result + c; Return result.
The computation of c depends on the field type:
boolean → c = (f ? 1 : 0) byte, char, short, int → c = (int) f long → c = (int)(f ^ (f >>> 32)) float → c = Float.floatToIntBits(f) double →
long l = Double.doubleToLongBits(f); c = (int)(l ^ (l >>> 32))object reference → c = f.hashCode() array → treat each element as a separate field and combine recursively.
Finally, combine all c values using the formula above and return the result.
1.3.3 Apache HashCodeBuilder
The Apache Commons HashCodeBuilder simplifies the implementation:
@Override
public int hashCode() {
return new HashCodeBuilder(83, 7)
.append(field1)
.append(field2)
.toHashCode();
}The two constructor arguments are non‑zero odd numbers that help reduce collisions.
1.4 Mutable Objects as Keys
It is recommended to use immutable objects as keys in collections. If a mutable object's state changes after it has been placed in a hash‑based collection, its hash code may change, causing the object to be stored in the wrong bucket and making look‑ups fail.
Example:
public class Employee {
private String name;
private int age;
// getters, setters omitted
@Override
public boolean equals(Object obj) {
if (obj instanceof Employee) {
Employee e = (Employee) obj;
return name.equals(e.name) && age == e.age;
}
return false;
}
@Override
public int hashCode() {
return name.length() + age;
}
public static void main(String[] args) {
Employee e = new Employee("muhammad", 24);
Map<Object, Object> m = new HashMap<>();
m.put(e, "value");
System.out.println(m.get(e)); // works
e.name = "abid"; // mutates key
System.out.println(m.get(e)); // fails
}
}To avoid this, either keep keys immutable or remove the object from the collection before mutating it and re‑insert it afterwards.
1.5 Memory Leaks with hashCode and equals
If equals() and hashCode() are not overridden correctly, a HashMap can retain references to objects indefinitely, leading to memory leaks and eventually an OutOfMemoryError. Example:
public class HashcodeLeakExample {
private String id;
public HashcodeLeakExample(String id) { this.id = id; }
public static void main(String[] args) {
Map<HashcodeLeakExample, String> map = new HashMap<>();
while (true) {
map.put(new HashcodeLeakExample("id"), "value");
}
}
}Because each new instance is considered a distinct key (default Object.equals() and hashCode()), the map never discards old entries, causing unbounded growth.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
