Java equals, hashCode and toString
🎧 Focus sounds
Soft beat sounds that many learners like while reading. Pick one, press Play, and keep studying.
Keep the volume low. These sounds are for comfort while studying, not a medical treatment. If you have epilepsy or are sensitive to pulsing sounds, use the background noise only.
In this lesson
Every Java class inherits three important methods from Object: toString(), equals() and hashCode(). Their default versions are rarely what you want for your own classes, and getting equals/hashCode wrong causes some of the sneakiest bugs in Java.
toString: readable objects#
class Book {
String title; String author;
Book(String title, String author) { this.title = title; this.author = author; }
}
System.out.println(new Book("Wings of Fire", "A. P. J. Abdul Kalam"));
// Book@6d06d69c ← class name + hash: useless
Override it:
@Override
public String toString() {
return "Book{title='" + title + "', author='" + author + "'}";
}
// Book{title='Wings of Fire', author='A. P. J. Abdul Kalam'}
toString is used by println, string concatenation, debuggers and logs. Always override it for data classes.
== vs equals#
==asks: are these the same object (same address)?equals()asks: are these objects meaningfully equal (same content)?
The default Object.equals is just ==. So two different Book objects with the same title and author are not equal until you override equals. (You saw the same issue with Strings and the string pool.)
Overriding equals correctly#
import java.util.Objects;
class Book {
private final String isbn;
private final String title;
Book(String isbn, String title) { this.isbn = isbn; this.title = title; }
@Override
public boolean equals(Object o) {
if (this == o) return true; // same object
if (!(o instanceof Book other)) return false; // null or different type
return isbn.equals(other.isbn); // compare what defines identity
}
@Override
public int hashCode() {
return Objects.hash(isbn); // same fields as equals!
}
@Override
public String toString() { return title + " [" + isbn + "]"; }
}
Note the parameter type is Object, not Book. Writing equals(Book o) overloads instead of overriding, and collections will never call it. @Override catches that mistake.
The contract#
- Reflexive:
a.equals(a)is true. - Symmetric: if
a.equals(b)thenb.equals(a). - Transitive: if
a.equals(b)andb.equals(c)thena.equals(c). - Consistent: same result every time if nothing changed.
a.equals(null)is false.- **If
a.equals(b), thena.hashCode() == b.hashCode().** (The reverse isn’t required: different objects may share a hash.)
Why hashCode matters: the HashSet bug#
HashMap and HashSet first use hashCode() to pick a bucket, then equals() to compare inside it (revisit the HashMap animation in the Java course). If you override equals but not hashCode, two equal objects usually land in different buckets:
import java.util.*;
public class HashBug {
static class Point {
final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override public boolean equals(Object o) {
return o instanceof Point p && p.x == x && p.y == y;
}
// hashCode NOT overridden!
}
static class FixedPoint {
final int x, y;
FixedPoint(int x, int y) { this.x = x; this.y = y; }
@Override public boolean equals(Object o) {
return o instanceof FixedPoint p && p.x == x && p.y == y;
}
@Override public int hashCode() { return Objects.hash(x, y); }
}
public static void main(String[] args) {
Set<Point> broken = new HashSet<>();
broken.add(new Point(1, 2));
System.out.println(broken.contains(new Point(1, 2))); // false (almost always)!
Set<FixedPoint> fixed = new HashSet<>();
fixed.add(new FixedPoint(1, 2));
System.out.println(fixed.contains(new FixedPoint(1, 2))); // true
}
}
Rules of thumb#
- **Always override
equalsandhashCodetogether, using the same fields**. - Use fields that don’t change (an id, ISBN, email). If a field used in
hashCodechanges while the object is in a HashSet, the object gets “lost” in the wrong bucket. - Use
Objects.equals(a, b)for null-safe field comparison andObjects.hash(...)for hashCode. - Let your IDE generate them (IntelliJ: Alt+Insert → equals() and hashCode()), or use a record, which does it for you.
Records do it automatically#
record Point(int x, int y) { }
Point a = new Point(1, 2), b = new Point(1, 2);
System.out.println(a.equals(b)); // true
System.out.println(a.hashCode() == b.hashCode()); // true
System.out.println(a); // Point[x=1, y=2]
Records generate equals, hashCode and toString from their components. More in the next lesson.
JPA/Hibernate entities are a special case: their id may be null until saved. Many teams compare entities by id only when it’s not null, or by a natural business key. You’ll see this in the JPA course.
Common mistakes#
equals(Book other)instead ofequals(Object o)(overloading, not overriding).- Overriding
equalswithouthashCode. - Using mutable fields in
hashCode. - Using
getClass() != o.getClass()vsinstanceofinconsistently across a hierarchy (pick one approach).
Exercise#
- Create an
Employeeclass where two employees are equal if they have the same employee id. Add both to aHashSetand check its size. - Remove the
hashCodeoverride and test again. What changes? - Write a
Moneyclass (amount in paise + currency) with properequals,hashCodeandtoStringlike₹12.50. - Rewrite
Moneyas a record and compare the amount of code.
Hint for 1
Two new Employee(101, "Asha") objects should give a set of size 1 only when both equals and hashCode use the id.
Interview corner#
- What’s the contract between equals and hashCode? Equal objects must have equal hash codes. Unequal objects may share a hash code.
- What happens if you override only equals? HashMap/HashSet lookups fail for equal objects because they land in different buckets.
- Can two different objects have the same hashCode? Yes, that’s a collision; equals decides.
- Why make keys immutable? If a key’s hash changes after insertion, the map can’t find it.
Summary#
- Override
toStringfor readable output. ==compares references;equalscompares meaning.- Override
equals(Object)andhashCode()together using the same immutable fields. - Records generate all three automatically.
Next: modern Java data types: records, enums and sealed classes.
Quick quiz
3 questions. Check what stuck and earn 2 XP for each right answer.
Progress is saved in this browser. Create a free account to keep it on every device.
Get new lessons in your inbox
One email when we publish a new tutorial or guide. No spam, unsubscribe in one click.