DEV Community

Thellu
Thellu

Posted on

🚨How Incorrect `equals()` and `hashCode()` Implementations Cause Memory Leaks in Java

When we talk about memory leaks in Java, many developers immediately think of listeners not being removed or caches growing indefinitely. But there's a more subtle and surprisingly common culprit: incorrect implementations of equals() and hashCode().

This post walks through how these two methods — when implemented poorly — can quietly wreak havoc on your application's memory.


The Basics: equals() and hashCode()

In Java, equals() and hashCode() are used to determine object equality and how objects are stored in hash-based collections like HashMap or HashSet.

A correct contract is:

  • If two objects are equal (a.equals(b) == true), they must return the same hashCode().
  • If two objects are not equal, it is not mandatory to return different hash codes — but doing so improves performance.

🔥 What Happens If You Break the Contract?

Let’s look at a real-world-style scenario:

import java.util.HashSet;
import java.util.Set;

public class User {
    private String username;

    public User(String username) {
        this.username = username;
    }

    // Oops! No equals() or hashCode() override!

    public static void main(String[] args) {
        Set<User> users = new HashSet<>();

        for (int i = 0; i < 1000000; i++) {
            users.add(new User("john_doe"));
        }

        System.out.println(users.size()); // Expecting 1? Nope.
    }
}
Enter fullscreen mode Exit fullscreen mode

Expected size? 1

Actual size? 1,000,000

Why? Because each User("john_doe") is a new object with the default hashCode() and equals() — meaning Java thinks they are all different.


💣 Where Memory Leaks Come In

Collections like HashMap and HashSet rely on these two methods for deduplication and key management. If you don’t override them properly, you can:

  • Create duplicate keys in maps
  • Grow collections indefinitely
  • Make it impossible to remove objects (since .remove(obj) depends on equals() working)

And if these collections live for the lifetime of your application — congratulations, you now have a memory leak.


A Real Example: Caching Gone Wrong

Don't think you'll never write stupid code like this, see this example:

Map<User, CachedResult> cache = new HashMap<>();

public CachedResult getResult(User user) {
    return cache.computeIfAbsent(user, this::expensiveOperation);
}
Enter fullscreen mode Exit fullscreen mode

If User doesn't correctly implement equals() and hashCode(), this cache will grow indefinitely, because Java can never recognize that the same user was already cached.


How to Fix It

Always override both equals() and hashCode() together.

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    User user = (User) o;
    return Objects.equals(username, user.username);
}

@Override
public int hashCode() {
    return Objects.hash(username);
}
Enter fullscreen mode Exit fullscreen mode

Or better yet, use Lombok:

@EqualsAndHashCode
public class User {
    private String username;
}
Enter fullscreen mode Exit fullscreen mode

Pro Tip: Watch Out in Unit Tests

Unit tests might not catch this kind of issue if you're only checking content — not behavior in collections. Be sure to test scenarios involving Map, Set, or deduplication logic.


Conclusion

It’s easy to forget the equals()/hashCode() contract until it’s too late. But when misused, they can cause memory leaks that are hard to track down.

So next time you're working with custom objects used as keys in maps or elements in sets — stop and ask: Do I really implement equals() and hashCode() correctly?

Top comments (0)