Learning goals
Implement equals, hashCode, and toString correctly for value semantics, use Objects helpers and records, and avoid broken collections behavior from contract violations.
Why override?
Default Object.equals compares reference identity (==). Value objects (money, coordinates, users keyed by id) need logical equality.
Point a = new Point(1, 2);
Point b = new Point(1, 2);
a.equals(b); // false unless overridden
hashCode must agree: equal objects → same hash code. Used by HashMap, HashSet.
toString aids debugging and logs.
equals contract
Reflexive, symmetric, transitive, consistent, and null-safe (return false, not NPE).
Template with Objects.equals:
public final class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point other)) return false;
return x == other.x && y == other.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
@Override
public String toString() {
return "Point[x=" + x + ", y=" + y + "]";
}
}
Pattern matching instanceof Point other (JDK 16+) avoids cast.
hashCode contract
- Equal objects → same hash.
- Unequal objects → hashes may collide (acceptable).
- Use
Objects.hash(field1, field2, ...)or multiply-add scheme for primitives.
If you override equals, you must override hashCode—or hash collections break silently.
Set<Point> set = new HashSet<>();
set.add(new Point(1, 2));
set.contains(new Point(1, 2)); // false if hashCode wrong
toString guidelines
Include class name and key fields; no sensitive data (passwords, tokens):
@Override
public String toString() {
return "User[id=" + id + ", email=" + email + "]";
}
IDEs generate boilerplate; review output for PII in production logs.
Records (generated for free)
record Point(int x, int y) {}
Compiler generates canonical constructor, accessors, equals, hashCode, toString. Prefer records for immutable value types.
Custom behavior still allowed:
record Email(String value) {
public Email {
Objects.requireNonNull(value);
if (!value.contains("@")) throw new IllegalArgumentException();
}
}
Inheritance caution
Subclasses complicate equals. getClass() vs instanceof:
getClass()— strict:Pointequals onlyPoint, not subclass.instanceof— Liskov-friendly for extensible hierarchies.
For entities with id:
@Override
public boolean equals(Object o) {
if (!(o instanceof Entity other)) return false;
return id != null && id.equals(other.id);
}
Never equate unrelated types.
Lombok / IDE generation
Tools auto-generate methods—still understand the contract for interviews and when excluding fields (transient, derived).
Common mistakes
- equals without hashCode — broken
HashMap/HashSet. - Mutable fields in equals/hashCode — object moves buckets when field changes; use immutable keys or identity.
equalsaccepts wrong type — always check withinstanceof.- Including mutable or circular refs in toString — stack overflow or huge logs.
- Comparing floats with
==in equals — useFloat.compareorDouble.compare.
Practice checkpoint
-
Implement equals/hashCode for
Money(long cents)value object.- Answer:
return cents == other.cents;andLong.hashCode(cents).
- Answer:
-
Two unequal objects can share hashCode—true or false?
- Answer: True (collision allowed).
-
Why
set.contains(new Point(1,2))fails after add if hashCode missing?- Answer: Set looks in wrong bucket; treats as different object.
-
What does
recordgive you automatically?- Answer: Constructor, accessors, equals, hashCode, toString.
Interview angles
- “equals vs ==?” —
==identity;equalslogical (overridable). - “hashCode contract?” — Consistent with equals; used for bucket indexing.
- “Can equal objects have different hash codes?” — No (contract violation if equal and different hash).