0Pricing
Java Academy · Lesson

The equals/hashCode Contract

Why both must agree.

The equals/hashCode Contract is a free Java Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Java Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Two Methods, One Contract

Every Java object inherits equals() and hashCode() from Object.

When you override one, you almost always must override the other. They form a binding contract that hash-based collections rely on.

public class Main {
    public static void main(String[] args) {
        Object a = new Object();
        Object b = new Object();
        System.out.println(a.equals(b));
        System.out.println(a.hashCode() == b.hashCode());
    }
}

The Core Rule

The contract states:

  • If a.equals(b) is true, then a.hashCode() must equal b.hashCode().
  • If hash codes differ, the objects are guaranteed unequal.

Equal objects must have equal hash codes. The reverse is not required.

public class Main {
    public static void main(String[] args) {
        String s1 = "hello";
        String s2 = "hel" + "lo";
        System.out.println(s1.equals(s2));
        System.out.println(s1.hashCode() == s2.hashCode());
    }
}

Unequal Objects May Share a Hash

Two unequal objects are allowed to share a hash code. That is just a collision, and the map handles it with equals().

So the only forbidden situation is: equal objects with different hash codes.

public class Main {
    public static void main(String[] args) {
        // Equal value, unequal objects, same hash is fine
        System.out.println("FB".hashCode() == "Ea".hashCode());
        System.out.println("FB".equals("Ea"));
    }
}

A Class That Overrides Only equals

Here is a broken class. It overrides equals() but not hashCode().

Two logically equal points now have different hash codes inherited from Object, violating the contract.

public class Main {
    static 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 p)) return false;
            return x == p.x && y == p.y;
        }
        // BUG: no hashCode override
    }
    public static void main(String[] args) {
        Point a = new Point(1, 2);
        Point b = new Point(1, 2);
        System.out.println("equals: " + a.equals(b));
        System.out.println("same hash: " + (a.hashCode() == b.hashCode()));
    }
}

The Bug in a HashSet

The broken class fails in a HashSet. The set looks in the bucket chosen by the wrong hash and never finds the equal element.

You end up with duplicates that should have been rejected.

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

public class Main {
    static 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 p)) return false;
            return x == p.x && y == p.y;
        }
    }
    public static void main(String[] args) {
        Set<Point> set = new HashSet<>();
        set.add(new Point(1, 2));
        set.add(new Point(1, 2));
        System.out.println("size = " + set.size());
    }
}

Fixing It

Override both methods and derive them from the same fields. Then equal objects share a hash and land in the same bucket.

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

public class Main {
    static 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 p)) return false;
            return x == p.x && y == p.y;
        }
        @Override public int hashCode() { return Objects.hash(x, y); }
    }
    public static void main(String[] args) {
        Set<Point> set = new HashSet<>();
        set.add(new Point(1, 2));
        set.add(new Point(1, 2));
        System.out.println("size = " + set.size());
    }
}

equals Must Be Reflexive

A correct equals() obeys four properties. The first is reflexive: x.equals(x) must be true.

This sounds obvious, but custom logic can break it if you forget to handle the same-reference case.

public class Main {
    public static void main(String[] args) {
        String x = "java";
        System.out.println(x.equals(x));
    }
}

Symmetric and Transitive

Two more rules:

  • Symmetric: if a.equals(b) then b.equals(a).
  • Transitive: if a.equals(b) and b.equals(c), then a.equals(c).

Mixing types across a class hierarchy often breaks symmetry, so compare classes carefully.

public class Main {
    public static void main(String[] args) {
        String a = "x", b = "x", c = "x";
        boolean symmetric = a.equals(b) == b.equals(a);
        boolean transitive = a.equals(b) && b.equals(c) && a.equals(c);
        System.out.println("symmetric: " + symmetric);
        System.out.println("transitive: " + transitive);
    }
}

Consistent

The fourth rule is consistent: repeated calls return the same result as long as the objects do not change.

This is why you should base equals and hashCode on immutable fields. Mutating a field used in hashCode after insertion corrupts the map.

import java.util.Objects;

public class Main {
    public static void main(String[] args) {
        // Immutable record gives consistent equals/hashCode automatically
        record Id(int value) {}
        Id id = new Id(7);
        System.out.println(Objects.equals(id, new Id(7)));
        System.out.println(id.hashCode() == new Id(7).hashCode());
    }
}

Records Do It For You

A Java record automatically generates equals() and hashCode() from all its components.

For value-like data, prefer records. The contract is satisfied for free and stays in sync.

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

public class Main {
    record Point(int x, int y) {}
    public static void main(String[] args) {
        Set<Point> set = new HashSet<>();
        set.add(new Point(1, 2));
        set.add(new Point(1, 2));
        System.out.println("size = " + set.size());
    }
}

The Mutable Key Trap

If you mutate a field used in hashCode after putting the object in a map, you can no longer find it.

The map looks in the old bucket; the object now hashes to a new one. The entry becomes a lost ghost.

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public class Main {
    static class Box {
        int id;
        Box(int id) { this.id = id; }
        @Override public boolean equals(Object o) {
            return o instanceof Box b && b.id == id;
        }
        @Override public int hashCode() { return Objects.hash(id); }
    }
    public static void main(String[] args) {
        Map<Box, String> m = new HashMap<>();
        Box key = new Box(1);
        m.put(key, "value");
        key.id = 99; // mutated after insertion
        System.out.println(m.get(key));
    }
}

Quick Check

Test your grasp of the contract.

Recap

You learned the equals/hashCode contract:

  • Equal objects must have equal hash codes.
  • equals must be reflexive, symmetric, transitive, and consistent.
  • Base both methods on the same immutable fields.
  • Records generate correct implementations for free.

Next, you will write your own correct hashCode by hand.

import java.util.Objects;

public class Main {
    public static void main(String[] args) {
        System.out.println("Contract recap: " + Objects.hash(1, 2, 3));
    }
}

Frequently asked questions

Is the “The equals/hashCode Contract” lesson free?

Yes — the full text of “The equals/hashCode Contract” is free to read here on the web, and the Java Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Java Academy course, upgrade to CoddyKit PRO.

What will I learn in “The equals/hashCode Contract”?

Why both must agree. You practise Java Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Java Academy?

No prior experience is required. Java Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “The equals/hashCode Contract” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Java Academy lesson?

Yes. Every Java Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. How HashMap Works
  2. The equals/hashCode Contract
  3. Implementing hashCode
  4. Treeification and Performance
← Back to Java Academy