Skip to content

Java · Object-Oriented Programming

Java equals & hashCode Contract — Identity, Equality, and Hash Collections

8 min read Updated 2026-06-20 Share:

Practice equals & hashCode interview questions

Why equals and hashCode matter

Almost every Java value class — String, Integer, LocalDate, your own domain objects — has to answer one deceptively hard question: when are two of these "the same"? Get the answer right and your objects drop cleanly into a HashMap or HashSet. Get it wrong and your collections silently lose data — no exception, no warning, just a contains that returns false for an object you swear you put in. This guide walks through the equals/hashCode contract, the design judgment behind it, and the traps that turn up in interviews.

== vs equals: identity vs equality

The first distinction is the one most beginners blur. == compares identity — for objects it asks "are these the same object in memory?" equals compares logical value, as the class defines it. The Object default equals is just ==, so it means nothing useful until you override it.

String a = new String("x");
String b = new String("x");
a == b;          // false — two distinct objects on the heap (identity)
a.equals(b);     // true  — same characters (value equality)

String c = "x", d = "x";
c == d;          // true  — literals are interned, so == happens to work

Using == on objects when you meant value equality is the classic String-comparison bug: it "works" on interned literals, then fails the moment text arrives from input, concatenation, or new. Rule of thumb: == for primitives and identity, equals for value.

The equals contract: five properties

A correct equals is not just "compare some fields." It must satisfy five properties, and sorted/hashed collections rely on every one of them:

  • Reflexivex.equals(x) is always true.
  • Symmetricx.equals(y) is true if and only if y.equals(x) is.
  • Transitive — if x.equals(y) and y.equals(z), then x.equals(z).
  • Consistent — repeated calls return the same result (no random or time-based inputs).
  • Non-nullx.equals(null) returns false and never throws.
@Override public boolean equals(Object o) {
  if (this == o) return true;             // reflexive fast-path
  if (!(o instanceof Point p)) return false; // handles null AND wrong type, non-throwing
  return x == p.x && y == p.y;            // symmetric & transitive by construction
}

The instanceof check does double duty: null instanceof Point is false, so it satisfies the non-null rule, and a wrong type is rejected up front. Compare the same fields in the same way on both sides and symmetry and transitivity fall out naturally.

The hashCode contract

hashCode returns an int used to bucket objects in hash-based collections. The contract binds it tightly to equals:

  1. Equal objects must have equal hash codes — if a.equals(b), then a.hashCode() == b.hashCode().
  2. The result must be consistent across calls (given unchanged state).
  3. Unequal objects may share a hash code — collisions are legal, just undesirable.
@Override public int hashCode() {
  return Objects.hash(x, y);   // same fields as equals -> consistent
}

Note the asymmetry: equal objects must collide on hash, but unequal objects are allowed to. That is why hashCode is a fast pre-filter, not a substitute for equals — the collection narrows to a bucket by hash, then confirms with equals.

What breaks in HashMap and HashSet

Here is the failure that the contract exists to prevent. A HashMap/HashSet finds a bucket by hashCode first, then walks that bucket calling equals. Override equals but forget hashCode, and two "equal" objects compute different identity hashes, land in different buckets, and the lookup never even reaches your equals.

class Key {
  final int id;
  Key(int id) { this.id = id; }
  @Override public boolean equals(Object o) {
    return o instanceof Key k && k.id == id;   // value equality...
  }
  // ...but NO hashCode override -> inherits Object's identity hash!
}

var set = new HashSet<Key>();
set.add(new Key(1));
set.contains(new Key(1));   // false! equal by equals, different hashCode -> wrong bucket

The collection isn't broken — it's doing exactly what you told it. Rule of thumb:equals and hashCode are a package deal. Override neither or both, and derive both from the same fields.

instanceof vs getClass: the symmetry trap

How you check the argument's type is a real design decision, because it interacts with inheritance and the symmetry rule. instanceof lets a subclass be equals to its parent; getClass() demands the exact same class.

// instanceof — flexible, but if a subclass adds state, base.equals(sub) and
// sub.equals(base) can disagree -> SYMMETRY BROKEN
if (!(o instanceof Point p)) return false;

// getClass — strict: a subclass instance is never equal to a base instance,
// which preserves symmetry but breaks Liskov substitution for equality
if (o == null || getClass() != o.getClass()) return false;

Consider a ColorPoint extends Point that adds a color. With instanceof, a Point would consider a ColorPoint equal (ignoring color) while the ColorPoint would not consider the Point equal (color differs) — asymmetric, which corrupts hash collections. Effective Java recommends instanceof plus making value classes final (or favoring composition over inheritance), which sidesteps the whole problem. Rule of thumb: prefer instanceof on a final or record value type.

Objects.equals and Objects.hash

You almost never need to hand-roll the null checks and bit-mixing. java.util.Objects gives you null-safe building blocks that keep equals and hashCode consistent with each other by construction.

@Override public boolean equals(Object o) {
  if (this == o) return true;
  if (!(o instanceof User u)) return false;
  return age == u.age
      && Objects.equals(name, u.name);   // null-safe: no NPE if name is null
}

@Override public int hashCode() {
  return Objects.hash(name, age);        // combines fields, order-sensitive
}

Objects.equals(a, b) treats two nulls as equal and never throws; Objects.hash(...) applies the proven 31 * result + field mixing across your fields. Rule of thumb: reach for Objects.equals/Objects.hash over manual boilerplate — concise, null-safe, and mutually consistent.

Base both on the same immutable fields

The single most important implementation rule: equals and hashCode must read the exact same fields, and those fields should ideally be immutable. If they diverge, the contract breaks. If they're mutable, the object's hash can change while it sits in a map — stranding it in the wrong bucket.

var map = new HashMap<List<Integer>, String>();
var key = new ArrayList<>(List.of(1));
map.put(key, "v");
key.add(2);          // mutated a field equals/hashCode depend on -> hash changed
map.get(key);        // null! the entry is stranded in its original bucket

This is why String, Integer, records, and enums make ideal keys — they're immutable, so their hash is fixed for life. Rule of thumb: use immutable keys; if a key must be mutable, never touch the equals/hashCode fields while it's in a collection.

Records: equals and hashCode for free

Since Java 16, the safest way to get a correct equals/hashCode is to not write them at all. A record auto-generates a canonical constructor, accessors, and value-based equals, hashCode, and toString from its components — no boilerplate, and no way to forget hashCode.

record Point(int x, int y) { }

new Point(1, 2).equals(new Point(1, 2));                      // true  — generated equals
new Point(1, 2).hashCode() == new Point(1, 2).hashCode();     // true  — consistent
new Point(1, 2).toString();                                   // "Point[x=1, y=2]"

Two records are equal exactly when all their components are equal. You can override the generated methods, but you almost never should. Rule of thumb: for an immutable value type, reach for a record before hand-writing equality — it gets the contract right by default.

A note on compareTo and TreeMap

One last consistency rule worth knowing: compareTo should agree with equals* (x.compareTo(y) == 0iffx.equals(y)). Sorted collections like TreeSet/TreeMapuse **ordering, notequals**, so a mismatch produces surprises — BigDecimal("1.0")andBigDecimal("1.00")are unequal byequals(scale differs) butcompareTo-equal, so a TreeSet` treats them as duplicates. Keep them consistent unless you have a documented reason not to.

Recap

The equals/hashCode contract is where Java's object model meets its collections. Use == for identity, equals for value. A correct equals is reflexive, symmetric, transitive, consistent, and non-null — properties that flow naturally from an instanceof guard and field-by-field comparison. Override hashCode whenever you override equals, base both on the same immutable fields, and remember that equal objects must share a hash code or HashMap/HashSet will silently lose them. Prefer instanceof on final value types to dodge the symmetry trap, lean on Objects.equals and Objects.hash, and — best of all — let a record generate the whole contract for you.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel