equals(), hashCode() and the Object contract
Every class extends java.lang.Object, which provides equals(), hashCode(), toString(), getClass() and a few more.
By default, equals() compares references. Override it when two objects holding the same data should count as equal.
The contract: if a.equals(b) is true, then a.hashCode() == b.hashCode() must also be true. Break it, and HashMap and HashSet will lose your objects.
Use Objects.equals and Objects.hash (Java 7) to write these safely, or let a record generate them for you.
The methods every object has
Every class extends Object, inheriting equals, hashCode, toString, getClass, clone, wait and notify. finalize is deprecated for removal; use try-with-resources or a Cleaner to release resources.
The equals contract
equals must be reflexive (a.equals(a)), symmetric, transitive, consistent, and return false for null. Compare the fields that define identity, and use getClass() or a final class to keep symmetry with subclasses.
@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;
}The hashCode contract
Objects that are equals must have the same hashCode, or HashMap and HashSet will lose them. Unequal objects may share a hash, but fewer collisions is faster. Objects.hash combines fields for you.
@Override
public int hashCode() {
return Objects.hash(x, y);
}A useful toString
The default toString prints the class name and a hash (Point@1b6d3586). Override it with the fields that help debugging and logs, but never include passwords or tokens.
Copying objects: clone vs copy constructors
clone() needs the Cloneable marker interface, makes a shallow copy by default (nested objects are shared) and is awkward to get right. Prefer a copy constructor or a static factory, and copy nested mutable objects yourself (a deep copy).
public class Cart {
private final List<String> items;
public Cart(List<String> items) { this.items = new ArrayList<>(items); }
public Cart(Cart other) { this(other.items); } // copy constructor (deep enough here)
}Records do it for you
A record generates equals, hashCode and toString from its components, correctly. For immutable data, a record is the simplest way to follow the Object contract.
record Point(int x, int y) {}
new Point(1, 2).equals(new Point(1, 2)); // true
new Point(1, 2).toString(); // "Point[x=1, y=2]"Example
public final class Isbn {
private final String code;
public Isbn(String code) { this.code = code; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Isbn other)) return false; // pattern matching, Java 16
return code.equals(other.code);
}
@Override
public int hashCode() { return Objects.hash(code); }
@Override
public String toString() { return "Isbn[" + code + "]"; }
}
Set<Isbn> set = new HashSet<>();
set.add(new Isbn("978-0134685991"));
System.out.println(set.contains(new Isbn("978-0134685991"))); // trueCommon mistake
Overriding equals but not hashCode. Two equal objects land in different HashMap buckets and contains() returns false.
Under the hood
For JPA entities this gets subtle: a generated id is null before the entity is saved, so a hash based on the id changes after persisting. Common approaches are a natural business key, or id-based equals with a constant hashCode() such as getClass().hashCode(). Never include lazy associations in equals, hashCode or toString; that triggers extra queries or LazyInitializationException.
Check yourself
If a.equals(b) is true, which must also be true?
How this connects
Know these first
Where this leads
Part of Job-ready backend developer, Crack the Java interview.
Was this lesson helpful?
Finished reading? Mark it complete to track your progress.