equals & hashCode Interview Questions & Answers
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.
==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. TheObjectdefault 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.
- Identity —
a == b: are they the same object in memory? - Equality —
a.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:
- Equal objects (
a.equals(b)) must have equal hash codes. equalsmust be reflexive, symmetric, transitive, and consistent.- 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:
- Reflexive —
x.equals(x)is true. - Symmetric —
x.equals(y)iffy.equals(x). - Transitive — if
x=yandy=z, thenx=z. - Consistent — repeated calls give the same result (no random/time inputs).
- Non-null —
x.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:
instanceofallows a subclass to beequalsto 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 ifais 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
intrange so buckets fill evenly (few collisions -> O(1) lookups). - Uses the same fields that
equalsuses, combined so order matters (Objects.hash/ the31 * result + fieldidiom).
@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 Object-Oriented Programming interview questions
More ways to practice
The self-quiz is live. Get notified when mock interviews and new question packs drop.