Equals and HashCode — The Contract, Implementation, and Best Practices
In this tutorial, you will learn about Equals and HashCode. We cover key concepts, practical examples, and best practices to help you master this topic.
The equals and hashCode methods define object equality and hash-based collection behavior in Java, with a strict contract that both must be overridden together. Every class inherits these methods from Object, but the default implementations compare references — which is rarely what you want for value objects.
What You'll Learn
- The
equals()andhashCode()contract - How to implement both correctly
- Common pitfalls and the
Objectsutility class - Automatic generation with Lombok and records
Why It Matters
Incorrect equals()/hashCode() implementations break HashMap, HashSet, HashTable, and any collection that relies on hashing. Objects that are "equal" must have the same hash code, or they will be stored in different buckets — making contains() return false when it should return true.
Real-World Use
JPA entities must implement equals()/hashCode() correctly to work with persistence contexts. DTOs and value objects use them for comparison. Every object stored in a HashSet or used as a HashMap key depends on them.
The Default Behavior
Object a = new Object();
Object b = new Object();
System.out.println(a.equals(b)); // false (different references)
System.out.println(a.equals(a)); // true (same reference)
For most value objects, reference equality is wrong. Two Person objects with the same name and age should be equal.
The equals() Contract
The equals() method must be:
- Reflexive:
x.equals(x)must be true - Symmetric:
x.equals(y)iffy.equals(x) - Transitive: if
x.equals(y)andy.equals(z), thenx.equals(z) - Consistent: multiple invocations return the same value (assuming no modification)
- Non-null:
x.equals(null)must be false
A Correct equals() Implementation
public class Person {
private String name;
private int age;
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person person = (Person) obj;
return age == person.age && Objects.equals(name, person.name);
}
}
The pattern:
- Check reference equality (optimization)
- Check null and class equality
- Cast and compare significant fields
- Use
Objects.equals()for nullable fields
The hashCode() Contract
- Consistency: Same object, multiple calls — same hash code (assuming no field changes)
- Equal objects, equal hash codes: If
a.equals(b), thena.hashCode() == b.hashCode() - Unequal objects CAN have same hash code (collisions are allowed)
A Correct hashCode() Implementation
@Override
public int hashCode() {
return Objects.hash(name, age);
}
Objects.hash() computes a hash code from the fields used in equals().
Manual Implementation
@Override
public int hashCode() {
int result = 17;
result = 31 * result + (name != null ? name.hashCode() : 0);
result = 31 * result + age;
return result;
}
The prime 31 (or 17/31) produces good distribution and is cheap to compute (JVM optimizes 31 * i to (i << 5) - i).
What Happens When You Break the Contract
public class BadPerson {
private String name;
@Override
public boolean equals(Object obj) {
// name-based equality — OK
if (!(obj instanceof BadPerson)) return false;
return Objects.equals(name, ((BadPerson) obj).name);
}
// No hashCode override!
}
Set<BadPerson> set = new HashSet<>();
set.add(new BadPerson("Alice"));
System.out.println(set.contains(new BadPerson("Alice"))); // probably false!
Because BadPerson does not override hashCode(), the two "Alice" objects have different hash codes (default Object.hashCode()), so they land in different buckets.
Using instanceof in equals()
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Person other)) return false; // pattern matching!
return age == other.age && Objects.equals(name, other.name);
}
Using instanceof with pattern matching (Java 16+) is cleaner than getClass(), but it violates the symmetry contract if subclasses add fields. The getClass() approach is stricter:
getClass():PersonequalsEmployeereturns false (stricter, safer for JPA)instanceof:Personcan equalEmployeeifEmployeeis aPerson(Liskov-friendly)
Lombok @EqualsAndHashCode
import lombok.EqualsAndHashCode;
@EqualsAndHashCode
public class Person {
private String name;
private int age;
}
Lombok generates both methods based on all non-static, non-transient fields. Use @EqualsAndHashCode.Exclude to exclude fields.
Records
Records automatically implement equals() and hashCode() based on all components:
public record Person(String name, int age) {}
Person p1 = new Person("Alice", 30);
Person p2 = new Person("Alice", 30);
System.out.println(p1.equals(p2)); // true (record does this for free)
Common Mistakes
- Overriding
equals()withouthashCode(). Hash-based collections break silently. - Using mutable fields in
hashCode(). If a field changes, the hash code changes, making the object "lost" in aHashSet. - Forgetting
nullhandling.name.equals(other.name)throwsNullPointerException. UseObjects.equals(). - Including irrelevant fields. Fields like
id(auto-generated) should often be excluded if equality is based on business keys. - Breaking symmetry with inheritance. A subclass that adds fields cannot maintain symmetry when
equals()usesinstanceof.
Practice Questions
1. What is the contract between equals() and hashCode()?
If a.equals(b) is true, then a.hashCode() == b.hashCode() must be true. The reverse is not required.
2. Why should you use Objects.equals(a, b) instead of a.equals(b)?
Objects.equals() handles null safely — if both are null, it returns true; if one is null, false. a.equals(b) throws NullPointerException if a is null.
3. What fields should be included in equals() and hashCode()?
The same fields should be used in both. Include fields that define business identity (e.g., email, username) and exclude auto-generated IDs or transient state.
4. How do records implement equals() and hashCode()?
The compiler generates both methods based on all record components. The implementation uses getClass() for type comparison.
5. What is the problem with using instanceof in equals() when there are subclasses?
If a subclass adds a field, a.equals(b) might be true while b.equals(a) is false (symmetry violated). Example: Person equals Employee via instanceof, but Employee.equals() checks the extra salary field.
Challenge Question:
Design a User class with fields email (business key), name, and lastLogin (transient). Write equals() and hashCode() using only email. Then create a Set<User> and verify that two users with the same email are considered equal even if names differ. Then add a PremiumUser subclass with an extra tier field — demonstrate the symmetry problem.
FAQ
Mini Project
Write a program EqualsDemo.java that:
- Creates a
Personclass with correctequals()/hashCode()usingObjects.equals()andObjects.hash() - Creates a
BadPersonclass withequals()but nohashCode() - Adds instances to both
HashSetandHashMap— demonstrate the bug - Adds a
Personwith a mutableList<String>field and shows the problem of mutating after insertion - Creates a record
PersonRecord(String name, int age)and shows that equals/hashCode work correctly - Uses
Lombok-style manual generation with@EqualsAndHashCodeannotation (simulate without Lombok)
What's Next
Equality is about comparing objects. But how do you order them? Lesson 29 covers Comparable and Comparator — natural ordering via Comparable, custom ordering via Comparator, the Comparator.comparing() Factory methods, and thenComparing() for chaining comparisons.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro