Skip to content
elephantoo

equals() & hashCode()

Lesson 29 of 43 14 min read

The equality contract, writing correct equals/hashCode, and why HashMap breaks without them.


When are two objects "the same"? Java has two answers:

  • Identity (==): are these two references pointing at the very same object in memory?
  • Equality (equals): do these two objects represent the same value?

Two String objects with the text "hello" are equal but not necessarily identical. For your own classes, Java can't guess what "equal" means. You define it by overriding equals(), and whenever you do, you must also override hashCode(). Getting this wrong is one of the most common sources of subtle bugs with HashMap and HashSet.

The default behaviour#

Object.equals simply uses ==. So without overriding it, two objects with identical data are not equal:

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

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

    public static void main(String[] args) {
        Point a = new Point(1, 2);
        Point b = new Point(1, 2);
        System.out.println(a == b);
        System.out.println(a.equals(b));      // Object.equals: same as ==

        Set<Point> set = new HashSet<>();
        set.add(a);
        set.add(b);
        System.out.println(set.size());       // 2 "duplicates"
        System.out.println(set.contains(new Point(1, 2)));
    }
}
Output
false
false
2
false

For entities with identity (a database connection, a thread) that's fine. For value objects (points, money, email addresses, keys) it is a bug waiting to happen.

The equals contract#

A correct equals must be:

  1. Reflexive: x.equals(x) is true.
  2. Symmetric: x.equals(y) ⇔ y.equals(x).
  3. Transitive: if x.equals(y) and y.equals(z), then x.equals(z).
  4. Consistent: repeated calls give the same answer while the objects don't change.
  5. Non-null: x.equals(null) is false (never throws).

The hashCode contract#

  1. If a.equals(b), then a.hashCode() == b.hashCode(). This is the critical rule.
  2. The same object must return the same hash code while its relevant fields don't change.
  3. Unequal objects may share a hash code (a collision). That is legal, just slower.

Why it matters: a HashMap first uses hashCode() to choose a bucket, and only then calls equals() on the entries in that bucket. If two equal objects have different hash codes, the map looks in the wrong bucket and never even calls your equals.

Writing equals and hashCode correctly#

GoodEquals.java
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

public class GoodEquals {
    static final class Point {
        private final int x, y;
        private final String label;

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

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;                      // same object: fast path
            if (!(o instanceof Point other)) return false;   // null or a different type
            return x == other.x
                && y == other.y
                && Objects.equals(label, other.label);       // null-safe comparison
        }

        @Override
        public int hashCode() {
            return Objects.hash(x, y, label);                // same fields as equals
        }

        @Override
        public String toString() { return label + "(" + x + "," + y + ")"; }
    }

    public static void main(String[] args) {
        Point a = new Point(1, 2, "A");
        Point b = new Point(1, 2, "A");
        System.out.println(a.equals(b) + " " + (a.hashCode() == b.hashCode()));

        Set<Point> set = new HashSet<>(Set.of(a));
        System.out.println(set.contains(b));

        Map<Point, String> names = new HashMap<>();
        names.put(a, "home");
        System.out.println(names.get(new Point(1, 2, "A")));
        System.out.println(a.equals(null) + " " + a.equals("A(1,2)"));
        System.out.println(new Point(0, 0, null).equals(new Point(0, 0, null)));
    }
}
Output
true true
true
home
false false
true

The recipe:

  1. Declare exactly public boolean equals(Object o) with @Override.
  2. Check this == o for a quick win.
  3. Use instanceof with a pattern variable (Java 16+) to check the type and cast in one step. It also returns false for null.
  4. Compare the significant fields: == for primitives, Objects.equals for objects (null-safe), Arrays.equals for arrays, Double.compare(a, b) == 0 for doubles.
  5. Build hashCode from the same fields, typically with Objects.hash(...).

Objects.hash is convenient but allocates a small array on each call. For hot code paths, a hand-written version is faster: int h = Integer.hashCode(x); h = 31 * h + Integer.hashCode(y); h = 31 * h + Objects.hashCode(label); return h;. IDEs can generate either form.

What goes wrong if you skip hashCode#

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

public class MissingHashCode {
    static class Email {
        final String address;
        Email(String address) { this.address = address; }

        @Override
        public boolean equals(Object o) {
            return o instanceof Email e && address.equalsIgnoreCase(e.address);
        }
        // hashCode NOT overridden!
    }

    public static void main(String[] args) {
        Email a = new Email("asha@example.com");
        Email b = new Email("ASHA@example.com");
        System.out.println(a.equals(b));

        Set<Email> set = new HashSet<>();
        set.add(a);
        set.add(b);
        System.out.println(set.size());      // almost certainly 2
    }
}
Output
true
2

The objects are equal, yet the set keeps both, because their default (identity-based) hash codes differ. The fix must hash consistently with the case-insensitive comparison: return address.toLowerCase().hashCode();.

The overloading trap#

Java
public boolean equals(Point other) { ... }   // WRONG: this is an overload

Collections call equals(Object), so they never see this method. @Override would turn this mistake into a compile error. Always use it.

Mutable keys: the disappearing entry#

If a field used in hashCode changes while the object is inside a HashSet or used as a HashMap key, the object becomes "lost": it sits in the bucket for its old hash code.

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

public class MutableKey {
    static class Tag {
        String name;
        Tag(String name) { this.name = name; }
        @Override public boolean equals(Object o) { return o instanceof Tag t && Objects.equals(name, t.name); }
        @Override public int hashCode() { return Objects.hashCode(name); }
    }

    public static void main(String[] args) {
        Tag t = new Tag("java");
        Set<Tag> tags = new HashSet<>();
        tags.add(t);

        t.name = "kotlin";                          // mutate while inside the set
        System.out.println(tags.contains(t));       // false: wrong bucket
        System.out.println(tags.size());            // but it's still in there!
    }
}
Output
false
1

Lesson: use immutable objects as keys and set elements, or at least never change the fields involved in equals/hashCode while they're stored.

Records do it for you#

For value classes, a record generates a correct equals, hashCode and toString from all its components:

RecordEquality.java
import java.util.Set;

public class RecordEquality {
    record Money(String currency, long paise) { }

    public static void main(String[] args) {
        Money a = new Money("INR", 500);
        Money b = new Money("INR", 500);
        System.out.println(a.equals(b) + " " + (a.hashCode() == b.hashCode()));
        System.out.println(Set.of(a).contains(b));
    }
}
Output
true true
true

This is one of the best reasons to use records for data carriers.

Equality and inheritance#

Equality across a class hierarchy is tricky. If ColorPoint extends Point adds a colour field, there is no way to make equals both symmetric and transitive when comparing Points with ColorPoints while also considering colour. Practical advice:

  • Make value classes final (records are final automatically).
  • Prefer composition (a ColorPoint that has a Point) over extending value classes.
  • If you must support subclasses, compare getClass() == o.getClass() instead of instanceof, so a Point and a ColorPoint are never equal.

Entities: equality by ID#

For objects that represent database rows (entities), equality is usually based on a stable identifier, not on every field:

Java
@Override
public boolean equals(Object o) {
    return o instanceof Customer c && id != null && id.equals(c.id);
}

@Override
public int hashCode() {
    return Customer.class.hashCode();   // constant: stays valid even when id is assigned later
}

JPA/Hibernate entities have special considerations; you'll meet them when you study Spring Data JPA.

equals, hashCode and compareTo together#

MethodUsed byMust agree with
equalsList.contains, indexOf, remove; HashSet/HashMaphashCode (mandatory)
hashCodeHashSet, HashMap, LinkedHashMapequals (mandatory)
compareTo / comparatorTreeSet, TreeMap, sortingequals (strongly recommended)

Common mistakes#

  • Overriding equals without hashCode (or the reverse).
  • Using different fields in equals and hashCode.
  • Writing equals(MyType other) instead of equals(Object o).
  • Using == to compare String or other object fields inside equals.
  • Mutating fields of objects stored in hash-based collections.
  • Including fields like timestamps or caches that don't define the value.

What's next#

That completes the intermediate module: you can design classes, handle errors and use collections professionally. Next, the advanced module begins with lambdas, functional interfaces and method references, the foundation of modern Java style.

Check your understanding

Quick quiz

0/3 answered
  1. 1.You override equals in a class but not hashCode. What is the likely consequence?

  2. 2.Which statement about the hashCode contract is true?

  3. 3.What is wrong with public boolean equals(Person other) in a class Person?

Finished reading?

Mark this lesson complete to track your progress.