Skip to content

equals & hashCode Interview Questions & Answers

15 questions Updated 2026-06-19 Share:

Java equals and hashCode interview questions — the equals/hashCode contract, why you must override both, == vs equals, identity vs equality, getClass vs instanceof, Objects helpers, mutable keys, compareTo consistency and toString.

Read the in-depth guideJava equals & hashCode Contract — Identity, Equality, and Hash Collections(opens in new tab)
15 of 15
  • == compares references for primitives' values and for objects' identity — are these the same object in memory?
  • equals() compares logical value, as defined by the class. The Object default is just ==, so it means nothing useful until overridden.
String a = new String("x"), b = new String("x");
a == b;            // false — two different objects
a.equals(b);       // true  — same characters

== on objects almost always indicates a bug when you meant value equality (the classic String comparison mistake). Rule of thumb: == for primitives and identity; equals() for value.

  • Identitya == b: are they the same object in memory?
  • Equalitya.equals(b): are they logically the same value, per the class's definition?
  • Some classes also expose ordering equivalence via compareTo == 0.
String a = new String("x"), b = new String("x");
a == b;            // false (identity)
a.equals(b);       // true  (equality)

BigDecimal x = new BigDecimal("1.0"), y = new BigDecimal("1.00");
x.equals(y);       // false! (scale differs)
x.compareTo(y) == 0; // true  (numeric equivalence)

BigDecimal is the classic gotcha where equals and compareTo disagree.

The Object implementation compares reference identity — it returns true only if both references point to the exact same object (this == other). So unless you override it, two distinct objects with identical state are "not equal."

class Point { int x, y; }
Point a = new Point(), b = new Point();   // both (0,0)
a.equals(b);   // false — default equals is identity

That's why value classes (String, Integer, LocalDate) override equals. Rule of thumb: if "two of these with the same contents should be equal," you must override equals (and hashCode).

If you override equals, you must override hashCode, obeying:

  1. Equal objects (a.equals(b)) must have equal hash codes.
  2. equals must be reflexive, symmetric, transitive, and consistent.
  3. Unequal objects may share a hash code (collisions are allowed).
class Point {
  final int x, y;
  @Override public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Point p)) return false;
    return x == p.x && y == p.y;
  }
  @Override public int hashCode() { return Objects.hash(x, y); }
}

Break the contract and hash-based collections silently misbehave: a HashMap/HashSet may fail to find an object whose hashCode doesn't match its equals. Use Objects.hash(...) / Objects.equals(...) to implement them.

Because hash-based collections (HashMap, HashSet) find a bucket by hashCode first, then check equals within it. If two equals objects have different hash codes, they land in different buckets and the collection never finds the match.

class Key {
  final int id;
  @Override public boolean equals(Object o) { /* by id */ return o instanceof Key k && k.id == id; }
  // no hashCode override -> uses identity hash!
}
var set = new HashSet<Key>();
set.add(new Key(1));
set.contains(new Key(1));   // false! equal but different hashCode

Rule of thumb: equals and hashCode are a package deal — override neither or both, derived from the same fields.

The equals contract requires five properties:

  • Reflexivex.equals(x) is true.
  • Symmetricx.equals(y) iff y.equals(x).
  • Transitive — if x=y and y=z, then x=z.
  • Consistent — repeated calls give the same result (no random/time inputs).
  • Non-nullx.equals(null) is false (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 + wrong type
  return x == p.x && y == p.y;           // symmetric & transitive by construction
}

Symmetry/transitivity are the ones that break with inheritance. Rule of thumb: compare the same fields both ways and reject null/other types up front.

It's a trade-off:

  • instanceof allows a subclass to be equals to its parent, but can break symmetry if a subclass adds state.
  • getClass() requires exact same class — preserves symmetry/transitivity but means no subclass instance is ever equal to a parent instance (breaks Liskov for equality).
// instanceof — flexible, but Sub vs Base symmetry is fragile
if (!(o instanceof Point p)) return false;
// getClass — strict, symmetric
if (o == null || getClass() != o.getClass()) return false;

Effective Java recommends instanceof plus making value classes final (or using composition) to sidestep the subclass problem. Rule of thumb: prefer instanceof on a final/record value type.

Objects provides null-safe utilities so you don't hand-roll boilerplate:

  • Objects.equals(a, b) — null-safe equality (no NPE if a is null).
  • Objects.hash(f1, f2, ...) — combines fields into one hash code.
  • Objects.requireNonNull(x) — guard constructor args.
@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
}
@Override public int hashCode() { return Objects.hash(name, age); }

Rule of thumb: use Objects.equals/Objects.hash for the fields — concise, null-safe, and consistent with each other.

A HashMap places an entry in a bucket based on the key's hashCode at insertion time. If you then mutate a field that hashCode/equals depend on, the key's hash changes — it now hashes to a different bucket, and lookups can no longer find it.

var map = new HashMap<List<Integer>, String>();
var key = new ArrayList<>(List.of(1));
map.put(key, "v");
key.add(2);                 // mutated — hashCode changed
map.get(key);               // null! stranded in the old bucket

Rule of thumb: use immutable keys (String, Integer, records, enums). If a key must be mutable, never change the fields used by equals/hashCode while it's in the map.

It should be (x.compareTo(y) == 0 iff x.equals(y)), and sorted collections assume it. When they disagree, TreeSet/TreeMap — which use compareTo, not equals — behave surprisingly.

var set = new TreeSet<BigDecimal>();
set.add(new BigDecimal("1.0"));
set.add(new BigDecimal("1.00"));   // compareTo == 0 -> treated as duplicate!
set.size();                         // 1, even though equals() says they differ

BigDecimal deliberately violates this (scale matters to equals but not compareTo). Rule of thumb: keep compareTo consistent with equals unless you have a documented reason — and know TreeSet/TreeMap use ordering, not equality.

== compares references. String literals are interned (shared from a pool), so == sometimes appears to work — but any String built at runtime (new, concatenation, input) is a different object, and == then returns false for equal text.

String a = "hi", b = "hi";
a == b;              // true — both the interned literal
String c = new String("hi");
a == c;              // false — different object
a.equals(c);         // true — same characters

Rule of thumb: always use .equals() (or Objects.equals) for string content; == for strings is a latent bug that "works" until the data comes from outside.

The default toString returns ClassName@hexHashCode — useless in logs. Override it to return a readable representation, which is automatically used in string concatenation, println, and debuggers.

class User {
  String name; int age;
  @Override public String toString() {
    return "User{name=" + name + ", age=" + age + "}";
  }
}
System.out.println(new User()); // User{name=null, age=0}

It's purely for human-readable diagnostics — don't parse it programmatically. IDEs and records can generate it for you.

A record auto-generates equals, hashCode, and toString from its components — value-based equality with no boilerplate and no risk of forgetting hashCode. Two records are equal iff all their components are equal.

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
new Point(1, 2).toString();                 // "Point[x=1, y=2]"

You can override them, but rarely should. Rule of thumb: for an immutable value type, a record is the safest way to get a correct equals/hashCode — reach for it before hand-writing them.

A good hashCode:

  • Is consistent with equals (equal objects -> equal hashes).
  • Distributes values across the int range so buckets fill evenly (few collisions -> O(1) lookups).
  • Uses the same fields that equals uses, combined so order matters (Objects.hash / the 31 * result + field idiom).
@Override public int hashCode() {
  return Objects.hash(name, age);   // good enough for almost everything
}
// returning a constant is *legal* but degrades HashMap to O(n)

Rule of thumb: Objects.hash(sameFieldsAsEquals) — never a constant, never a field excluded from equals.

clone() is widely considered broken: it relies on the Cloneable marker interface, bypasses constructors, does a shallow copy by default (shared mutable sub-objects), and has an awkward protected/checked-exception design.

// Preferred: a copy constructor or static factory
record Point(int x, int y) { }
Point copy = new Point(p.x(), p.y());          // explicit, clear

class Box { Box(Box other) { /* copy fields, deep where needed */ } }

Rule of thumb: prefer a copy constructor or static factory (copyOf) over clone() — they're explicit, work with final fields, and let you control shallow vs deep copying.

More ways to practice

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

or
Join our WhatsApp Channel