Home/Learn/Java A–Z/hashCode and equals Contract

hashCode and equals Contract

Intermediate
OOP Deep Dive

Correctly implementing hashCode and equals is critical for objects used as HashMap keys or in HashSets — violating the contract causes subtle bugs.

Overview

The equals/hashCode contract is one of the most important rules in Java: if two objects are equal (equals() returns true), they MUST have the same hashCode. The reverse is not required — hash collisions are allowed. Violating this contract causes objects to "disappear" in HashMaps and HashSets. Java 7+ provides Objects.hash() and Objects.equals() as utilities. Java records automatically generate correct implementations.

The equals/hashCode Contract

equals() must be: reflexive (a.equals(a)), symmetric (a.equals(b) == b.equals(a)), transitive, consistent, and a.equals(null) == false.

hashCode() contract: (1) consistent across calls, (2) if a.equals(b) then a.hashCode() == b.hashCode(). Note: a.hashCode() == b.hashCode() does NOT imply a.equals(b) — collisions are fine.

BrokenContract.java
// BROKEN — equals without hashCode
public class BrokenPoint {
    int x, y;
    @Override
    public boolean equals(Object o) {
        if (!(o instanceof BrokenPoint p)) return false;
        return x == p.x && y == p.y;
    }
    // hashCode not overridden — uses Object's identity hash
}

BrokenPoint p1 = new BrokenPoint(1, 2);
BrokenPoint p2 = new BrokenPoint(1, 2);
System.out.println(p1.equals(p2));    // true
System.out.println(p1.hashCode() == p2.hashCode()); // false (probably)

Set<BrokenPoint> set = new HashSet<>();
set.add(p1);
set.contains(p2); // FALSE — looks in wrong bucket!

Map<BrokenPoint, String> map = new HashMap<>();
map.put(p1, "origin");
map.get(p2); // NULL — same bug

Correct Implementation

Use all fields that participate in equals in hashCode too. Objects.hash() combines multiple fields cleanly. For performance-critical code, cache the hash if the object is immutable.

Java 14+ records automatically generate correct equals and hashCode based on all components.

CorrectImpl.java
public final class Point {
    private final int x;
    private final int y;

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

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;               // same reference — fast path
        if (!(o instanceof Point p)) return false; // null + type check
        return x == p.x && y == p.y;             // field comparison
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y); // combines fields with 31* polynomial
    }
}

// Cached hashCode for immutable objects (String-style)
public final class ImmutableKey {
    private final String a;
    private final int b;
    private int cachedHash; // 0 means not computed

    @Override
    public int hashCode() {
        int h = cachedHash;
        if (h == 0) {
            h = Objects.hash(a, b);
            cachedHash = h;
        }
        return h;
    }
}

// Records — automatic correct implementation
record Point(int x, int y) {}  // equals, hashCode, toString all generated

HashMap Internals and Hash Collisions

HashMap uses hashCode to find the bucket (array index = hash & (capacity-1)), then equals to find the exact key within the bucket. Collisions place multiple entries in the same bucket as a linked list; Java 8 converts to a tree (Red-Black) when a bucket exceeds 8 entries.

A poor hashCode that returns a constant causes every key to collide — HashMap degrades to O(n) linked list lookups.

HashMapInternals.java
// HashMap lookup process
// 1. Compute key.hashCode()
// 2. Spread bits: (h = key.hashCode()) ^ (h >>> 16)
// 3. bucket = hash & (table.length - 1)
// 4. Walk bucket (list or tree) using key.equals()

// Good hashCode — distributes evenly
// Objects.hash() uses: s[0]*31^(n-1) + s[1]*31^(n-2) + ...
// 31 is prime, distributes well, easily JIT-optimised

// BAD hashCode — constant = all keys in one bucket = O(n) lookups
@Override public int hashCode() { return 42; } // NEVER do this

// BAD hashCode — only x = collisions for all points with same x
@Override public int hashCode() { return x; }

// Verifying distribution
Map<Integer, Long> distribution = IntStream.range(0, 10_000)
    .mapToObj(i -> new Point(i % 100, i / 100))
    .collect(Collectors.groupingBy(
        p -> (p.hashCode() & 0x7FFFFFFF) % 16,
        Collectors.counting()));
// Ideally: ~625 entries per bucket (10000 / 16)

Key Points to Remember

  • equals() true → hashCode() must be equal. hashCode() equal does NOT imply equals() true.
  • Always override hashCode when you override equals — IDEs and Lombok do this automatically.
  • Use Objects.hash(field1, field2, ...) for a clean, collision-resistant hashCode.
  • Cache hashCode in immutable objects for performance (see String).
  • Bad hashCode (e.g. constant) degrades HashMap to O(n) — evenly distributing hashes is important.

Practice hashCode and equals Contract in the Playground

Run and modify code directly in your browser - no setup needed.

Interview Questions

Sign in to ask Aria
1

What is the hashCode/equals contract in Java?

MediumAmazon
2

What happens if you override equals but not hashCode?

MediumGoogle
3

How does HashMap use hashCode and equals internally?

MediumOracle
4

Why does Java 8 convert HashMap bucket lists to trees?

HardMicrosoft
5

Can two objects have the same hashCode but not be equal?

EasyTCS

Ask Aria about hashCode and equals Contract

Your personal AI tutor — ask anything about this concept