JAVA Contents

equals/hashCode, Collections Bugs

Implement equals/hashCode correctly to prevent silent collection corruption, avoid mutable equality keys, and ensure predictable behavior and performance in HashMap/HashSet in production.

On this page

equals/hashCode is a correctness and performance contract

In production systems, incorrect equality is dangerous because it often does not crash. It silently corrupts behavior: caches miss, sets contain duplicates, maps cannot find keys, and performance degrades due to poor hashing. You must treat equals/hashCode as a strict contract.

The contract (what must always be true)

Java equality must satisfy:

  • Reflexive: x.equals(x) is true
  • Symmetric: x.equals(y) == y.equals(x)
  • Transitive: if x==y and y==z then x==z
  • Consistent: repeated calls return the same result if state does not change
  • Non-null: x.equals(null) is false

And the key rule linking both methods:

  • If x.equals(y) is true, then x.hashCode() == y.hashCode() must be true.

Why HashMap/HashSet depends on this

Hash-based collections first use hashCode to find a bucket, then use equals to find the exact element. If either is wrong, behavior breaks.

Production failure scenario: key becomes unreachable

This happens when hashCode depends on mutable fields.

import java.util.*;

public final class User {
  private String email; // mutable
  private final String id;

  public User(String id, String email) {
    this.id = id;
    this.email = email;
  }

  public void setEmail(String email) { this.email = email; }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof User)) return false;
    User other = (User) o;
    return email.equals(other.email); // uses mutable field
  }

  @Override
  public int hashCode() {
    return email.hashCode(); // uses mutable field
  }
}
Map<User, String> map = new HashMap<>();
User u = new User("1", "a@example.com");

map.put(u, "value");
u.setEmail("b@example.com");

System.out.println(map.get(u)); // null (key is now in a different bucket)

This is silent corruption. In production it shows up as cache misses or duplicate entries.

Production rule: equality keys must be immutable

  • If the object is used as a key in a HashMap, the fields used by equals/hashCode must never change.
  • Prefer value objects (immutable) for keys: UserId, OrderId, Email (if immutable and normalized).

Value objects: correct equality by value

A value object is equal if its values are equal. Records are great for this.

public record UserId(String value) {
  public UserId {
    if (value == null || value.isBlank()) throw new IllegalArgumentException("blank");
  }
}

Records generate equals/hashCode correctly based on components.

Entities: equality by identity (usually)

Entities represent things that can change. In many systems, entity equality should be based on stable identity (id), not on mutable attributes like name or email.

public final class User {
  private final String id;
  private String name;

  public User(String id, String name) { this.id = id; this.name = name; }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof User)) return false;
    return id.equals(((User) o).id);
  }

  @Override
  public int hashCode() {
    return id.hashCode();
  }
}

Inheritance pitfalls: equals can break symmetry

Inheritance + equals is one of the hardest traps. The classic failure is symmetry break.

Problem example

class Point {
  final int x, y;
  Point(int x, int y) { this.x = x; this.y = y; }

  @Override
  public boolean equals(Object o) {
    if (!(o instanceof Point)) return false;
    Point p = (Point) o;
    return x == p.x && y == p.y;
  }

  @Override
  public int hashCode() { return 31 * x + y; }
}

class ColorPoint extends Point {
  final String color;
  ColorPoint(int x, int y, String color) { super(x, y); this.color = color; }

  @Override
  public boolean equals(Object o) {
    if (!(o instanceof ColorPoint)) return false;
    ColorPoint cp = (ColorPoint) o;
    return super.equals(o) && color.equals(cp.color);
  }
}

Now:

  • new Point(1,2).equals(new ColorPoint(1,2,"red")) is true (Point ignores color)
  • new ColorPoint(1,2,"red").equals(new Point(1,2)) is false (ColorPoint requires ColorPoint)

Symmetry is broken.

Production guidance

  • Prefer composition over inheritance for domain types.
  • If you must use inheritance, define equality rules very carefully and document them.
  • For value types, records and final classes drastically reduce risk.

Performance: hash distribution matters

Even if correctness is right, poor hash distribution causes performance issues (many collisions). In high QPS caches, this can become CPU spikes.

Common performance anti-pattern

@Override
public int hashCode() { return 1; }

This turns HashMap into near-linked-list behavior under collision. Always compute hash based on fields used in equals.

Testing equals/hashCode (cheap and valuable)

Write small unit tests for equality contracts, especially for key types and value objects.

UserId a = new UserId("123");
UserId b = new UserId("123");
UserId c = new UserId("456");

assert a.equals(b);
assert a.hashCode() == b.hashCode();
assert !a.equals(c);
assert !a.equals(null);

Checklist

  • Ensure equals is reflexive, symmetric, transitive, consistent, and non-null.
  • If equals returns true, hashCode must match.
  • Never base equality on mutable fields if object is used in hash-based collections.
  • Prefer identity-based equality for entities; value-based equality for value objects.
  • Avoid inheritance-based equals; prefer final classes/records or composition.
  • Keep hashCode reasonably distributed for performance.

Final principle

If your equals/hashCode is wrong, your collections will lie to you — and production will punish you quietly.