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:
- Reflexive —
x.equals(x)is alwaystrue. - Symmetric —
x.equals(y)istrueif and only ify.equals(x)is. - Transitive — if
x.equals(y)andy.equals(z), thenx.equals(z). - Consistent — repeated calls return the same result (no random or time-based inputs).
- Non-null —
x.equals(null)returnsfalseand 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:
- Equal objects must have equal hash codes — if
a.equals(b), thena.hashCode() == b.hashCode(). - The result must be consistent across calls (given unchanged state).
- 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.