equals() & hashCode()
The equality contract, writing correct equals/hashCode, and why HashMap breaks without them.
When are two objects "the same"? Java has two answers:
- Identity (
==): are these two references pointing at the very same object in memory? - Equality (
equals): do these two objects represent the same value?
Two String objects with the text "hello" are equal but not necessarily identical. For your own classes, Java can't guess what "equal" means. You define it by overriding equals(), and whenever you do, you must also override hashCode(). Getting this wrong is one of the most common sources of subtle bugs with HashMap and HashSet.
The default behaviour#
Object.equals simply uses ==. So without overriding it, two objects with identical data are not equal:
For entities with identity (a database connection, a thread) that's fine. For value objects (points, money, email addresses, keys) it is a bug waiting to happen.
The equals contract#
A correct equals must be:
- Reflexive:
x.equals(x)istrue. - Symmetric:
x.equals(y)⇔y.equals(x). - Transitive: if
x.equals(y)andy.equals(z), thenx.equals(z). - Consistent: repeated calls give the same answer while the objects don't change.
- Non-null:
x.equals(null)isfalse(never throws).
The hashCode contract#
- If
a.equals(b), thena.hashCode() == b.hashCode(). This is the critical rule. - The same object must return the same hash code while its relevant fields don't change.
- Unequal objects may share a hash code (a collision). That is legal, just slower.
Why it matters: a HashMap first uses hashCode() to choose a bucket, and only then calls equals() on the entries in that bucket. If two equal objects have different hash codes, the map looks in the wrong bucket and never even calls your equals.
Writing equals and hashCode correctly#
The recipe:
- Declare exactly
public boolean equals(Object o)with@Override. - Check
this == ofor a quick win. - Use
instanceofwith a pattern variable (Java 16+) to check the type and cast in one step. It also returnsfalsefornull. - Compare the significant fields:
==for primitives,Objects.equalsfor objects (null-safe),Arrays.equalsfor arrays,Double.compare(a, b) == 0fordoubles. - Build
hashCodefrom the same fields, typically withObjects.hash(...).
Objects.hashis convenient but allocates a small array on each call. For hot code paths, a hand-written version is faster:int h = Integer.hashCode(x); h = 31 * h + Integer.hashCode(y); h = 31 * h + Objects.hashCode(label); return h;. IDEs can generate either form.
What goes wrong if you skip hashCode#
The objects are equal, yet the set keeps both, because their default (identity-based) hash codes differ. The fix must hash consistently with the case-insensitive comparison: return address.toLowerCase().hashCode();.
The overloading trap#
Collections call equals(Object), so they never see this method. @Override would turn this mistake into a compile error. Always use it.
Mutable keys: the disappearing entry#
If a field used in hashCode changes while the object is inside a HashSet or used as a HashMap key, the object becomes "lost": it sits in the bucket for its old hash code.
Lesson: use immutable objects as keys and set elements, or at least never change the fields involved in equals/hashCode while they're stored.
Records do it for you#
For value classes, a record generates a correct equals, hashCode and toString from all its components:
This is one of the best reasons to use records for data carriers.
Equality and inheritance#
Equality across a class hierarchy is tricky. If ColorPoint extends Point adds a colour field, there is no way to make equals both symmetric and transitive when comparing Points with ColorPoints while also considering colour. Practical advice:
- Make value classes
final(records are final automatically). - Prefer composition (a
ColorPointthat has aPoint) over extending value classes. - If you must support subclasses, compare
getClass() == o.getClass()instead ofinstanceof, so aPointand aColorPointare never equal.
Entities: equality by ID#
For objects that represent database rows (entities), equality is usually based on a stable identifier, not on every field:
JPA/Hibernate entities have special considerations; you'll meet them when you study Spring Data JPA.
equals, hashCode and compareTo together#
Common mistakes#
- Overriding
equalswithouthashCode(or the reverse). - Using different fields in
equalsandhashCode. - Writing
equals(MyType other)instead ofequals(Object o). - Using
==to compareStringor other object fields insideequals. - Mutating fields of objects stored in hash-based collections.
- Including fields like timestamps or caches that don't define the value.
What's next#
That completes the intermediate module: you can design classes, handle errors and use collections professionally. Next, the advanced module begins with lambdas, functional interfaces and method references, the foundation of modern Java style.
Check your understanding
Quick quiz
1.You override
equalsin a class but nothashCode. What is the likely consequence?2.Which statement about the hashCode contract is true?
3.What is wrong with
public boolean equals(Person other)in a classPerson?
Finished reading?
Mark this lesson complete to track your progress.