Object Equality and Hash Codes in Java
A bank identifies a customer by their PAN. Two Customer objects loaded at different times, but with the same PAN, are the same customer. Java doesn't know that: by default, equals says two objects are equal only if they're the same object in memory. You teach Java your idea of equality by overriding equals(), and you must override hashCode() with it, or HashSet and HashMap silently misbehave.
Three Versions Side by Side
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
public class Contract {
static class PlainId {
final String pan;
PlainId(String pan) { this.pan = pan; }
}
static class EqualsOnly {
final String pan;
EqualsOnly(String pan) { this.pan = pan; }
@Override
public boolean equals(Object o) {
return o instanceof EqualsOnly other && pan.equals(other.pan);
}
}
static class Customer {
private final String pan;
private final String name;
Customer(String pan, String name) {
this.pan = pan;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Customer other)) return false;
return pan.equals(other.pan);
}
@Override
public int hashCode() {
return Objects.hash(pan);
}
}
public static void main(String[] args) {
Set<PlainId> a = new HashSet<>();
a.add(new PlainId("ABCDE1234F"));
a.add(new PlainId("ABCDE1234F"));
Set<EqualsOnly> b = new HashSet<>();
b.add(new EqualsOnly("ABCDE1234F"));
b.add(new EqualsOnly("ABCDE1234F"));
Set<Customer> c = new HashSet<>();
c.add(new Customer("ABCDE1234F", "Navika"));
c.add(new Customer("ABCDE1234F", "Navika Anamaneni"));
System.out.println("No equals, no hashCode: " + a.size());
System.out.println("equals only: " + b.size());
System.out.println("equals and hashCode: " + c.size());
System.out.println(c.contains(new Customer("ABCDE1234F", "x")));
}
}No equals, no hashCode: 2
equals only: 2
equals and hashCode: 1
trueHow it works
PlainIdoverrides nothing. Two objects with the same PAN are different objects, so the set keeps both.EqualsOnlyoverridesequalsbut nothashCode. The objects are "equal", yet the set still keeps both.HashSetlooks athashCodefirst, and the two default hash codes differ, so it never even callsequals.Customeroverrides both, using the PAN for each. The set sees a duplicate and keeps one, andcontainsfinds a matching customer.
How HashSet and HashMap Use Them
A hash table is an array of buckets. To find an object it:
- Calls
hashCode()and turns the number into a bucket index. - Looks only in that bucket, comparing hash codes and then calling
equals().
So hashCode decides where to look and equals decides whether it's there. If equal objects have different hash codes, the table looks in the wrong bucket. A later lesson on HashMap internals goes deeper.
The Contract
equals must be:
| Property | Meaning |
|---|---|
| Reflexive | x.equals(x) is true |
| Symmetric | If x.equals(y), then y.equals(x) |
| Transitive | If x.equals(y) and y.equals(z), then x.equals(z) |
| Consistent | Repeated calls give the same answer while the fields don't change |
| Null-safe | x.equals(null) is false, never an exception |
hashCode must:
- Return the same number every time for an unchanged object.
- Return equal numbers for equal objects.
- Unequal objects may share a hash code (a collision), but fewer collisions mean faster tables.
Writing Them Correctly
The Customer methods above follow a standard recipe:
if (this == o) return true;is a quick check for the same object.if (!(o instanceof Customer other)) return false;handlesnulland other types, and gives a typed variable.- Compare the identifying fields:
Objects.equals(a, b)for objects that may be null,==for primitives,Double.comparefor doubles. - In
hashCode, use the same fields:Objects.hash(pan).
instanceof or getClass()?
| Check | Effect | Use when |
|---|---|---|
o instanceof Customer other | Subclass objects can equal parent objects | The class is final, or subclasses don't add identity fields |
getClass() != o.getClass() | Only exactly the same class can be equal | Subclasses add fields that matter for equality |
Mixing them carelessly breaks symmetry: a parent saying "yes" to a child that says "no" back. Making value classes final (or using records) avoids the question entirely.
Common Mistakes
Writing equals(Customer other). That overloads instead of overriding; the parameter must be Object (see "Method Overloading vs Overriding").
Using different fields in equals and hashCode. Equal objects then get different hash codes.
Including mutable or derived fields. Use the fields that truly identify the object.
Interview Corner
What is the contract between equals() and hashCode()?
If two objects are equal according to equals, they must return the same hashCode. The reverse isn't required: unequal objects may share a hash code. hashCode must also stay the same for an object as long as the fields used in equals don't change. equals itself must be reflexive, symmetric, transitive, consistent, and return false for null.
What happens if you override equals() but not hashCode()?
Equal objects keep different default hash codes, so hash-based collections treat them as different: a HashSet stores duplicates, and HashMap.get with an equal key returns null, because the lookup goes to the bucket chosen by the hash code and never finds the stored object.
Can two unequal objects have the same hash code?
Yes, that's a collision, and it's allowed. Hash tables handle it by comparing with equals inside the bucket. Too many collisions only slow the table down.
Practice
- Easy: Write a
Bookclass equal by ISBN only, withequalsandhashCode. Add two copies of the same ISBN to aHashSetand print its size. - Harder: Put a
Customerwith a mutablepaninto aHashSet, change its PAN, and printcontainsbefore and after. Explain the result.
Key Takeaways
Was this tutorial helpful?