1. Introduction
How we compare objects
In Java, objects are not just data; each has its own address in memory. The == operator answers the question “is this the same box?”, i.e., it compares references (addresses), not contents. The equals method is intended to compare contents. By default, if you don’t override it, equals behaves like ==.
Person p1 = new Person("Ivan", 20);
Person p2 = new Person("Ivan", 20);
System.out.println(p1 == p2); // false — these are different objects in memory!
For two different objects to be considered equal by data (e.g., all significant fields match), you need to override equals. And if you plan to use the objects in hash-based collections, be sure to correctly override hashCode along with it.
class Person {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true; // this is the same object
if (o == null || getClass() != o.getClass()) return false; // check class
Person person = (Person) o; // cast to the required type
return age == person.age && name.equals(person.name); // compare fields
}
@Override
public int hashCode() {
return Objects.hash(name, age); // to work with HashSet/HashMap
}
}
Such overriding is critical for correct work with HashSet/HashMap: without equals and hashCode, these collections will consider even data-identical objects different.
The equivalence relation in equals depends on your requirements: you can compare by all fields, by a subset of fields (e.g., only email in User)—what matters is consistency and adherence to the contract.
Where is this especially important?
- In hash table–based collections: HashSet, HashMap, LinkedHashSet, and others.
- When searching for and removing elements in collections: without a correct equals, the desired object may “not be found”.
- In business logic: for example, two User objects with the same email should be considered the same user.
hashCode — what is it for?
Hash-based collections (e.g., HashSet, HashMap) use hash tables. The hashCode method computes an integer—the “bucket address” where the object will go. If two objects are equal according to equals, their hashCode values must match. Violate this rule, and collections start behaving unpredictably.
2. The equals and hashCode contract
The equals contract
Main requirements for equals behavior:
- Reflexivity: a.equals(a) is always true.
- Symmetry: if a.equals(b) is true, then b.equals(a) is also true.
- Transitivity: if a.equals(b) and b.equals(c), then a.equals(c) is also true.
- Consistency: if objects do not change, the result of calls remains stable.
- Null comparison: any non-null object is not equal to null.
The hashCode contract
- If two objects are equal according to equals, their hashCode values are equal.
- If objects are not equal, their hash codes may still collide (collisions are allowed but undesirable).
- As long as the object does not change logically, its hashCode should remain constant.
In other words, matching hashCode values are necessary but not sufficient for equality: the same hash does not guarantee equality according to equals.
3. Implementing equals and hashCode: an example
Consider the Person class where equality is defined by the name and age fields.
public class Person {
private String name;
private int age;
// Constructor, getters, setters...
@Override
public boolean equals(Object o) {
if (this == o) return true; // Reference comparison
if (o == null || getClass() != o.getClass()) return false; // Class check
Person person = (Person) o; // Cast
// Compare fields
return age == person.age &&
(name != null ? name.equals(person.name) : person.name == null);
}
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age; // 31 is a common choice of prime number
return result;
}
}
- Start with cheap checks: reference and class.
- Then compare the significant fields.
- In hashCode, the prime 31 is used to reduce collisions.
Using Objects.equals and Objects.hash
Since Java 7, the Objects class simplifies code and makes it more null-safe:
import java.util.Objects;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
4. equals, hashCode, and compareTo: how they relate
How are equals and compareTo related?
The Comparable interface defines the compareTo method, which returns a negative/zero/positive number for “less/equal/greater”. Ideally, a.compareTo(b) == 0 should imply a.equals(b). The reverse is not required.
If you violate consistency (for example, compareTo compares only by age, while equals compares by name and age), sorted collections like TreeSet/TreeMap may behave unexpectedly: objects are considered “equal” with respect to ordering but not equal by content.
equals and hashCode in collections
- In HashSet and HashMap, add/search/remove operations rely on a correct implementation of equals and hashCode.
- Without overriding them, these collections treat objects as “different” even if their data is identical.
5. Examples: how this works in collections
HashSet: storing unique objects
Set<Person> people = new HashSet<>();
people.add(new Person("Ivan", 20));
people.add(new Person("Ivan", 20)); // Duplicate
System.out.println(people.size()); // 1 if equals/hashCode are implemented correctly
Without a correct equals/hashCode, both objects will end up in the set.
HashMap: lookup by key
Map<Person, String> map = new HashMap<>();
Person p1 = new Person("Anna", 25);
Person p2 = new Person("Anna", 25);
map.put(p1, "User 1");
System.out.println(map.get(p2)); // "User 1" if equals/hashCode are implemented correctly
Without the contract, the collection will return null—these are “different” keys to it.
6. Best practices: implementation tips
- Include in equals/hashCode all fields that define the object’s identity.
- Do not use mutable fields (that change after insertion into collections) when computing hashCode.
- Let your IDE generate these methods—fewer chances for typos.
- In equals, first check this == o, then the class, then the fields.
- Use Objects.equals to compare object fields.
- For the hash code, use Objects.hash or the well-tested pattern with the multiplier 31.
7. Useful nuances
Why can’t you rely on hashCode alone?
Collisions are inevitable: different objects can have the same hashCode. The hash is only a fast hint for the bucket; the final decision about equality is made by equals.
Can you avoid overriding equals and hashCode?
Only if you are sure that objects will never be compared by content and will not become keys/unique elements in collections. In practice, this is rare.
Difference between ==, equals, and compareTo
| Operator/method | What does it compare? | What is it for? |
|---|---|---|
|
References (memory addresses) | Check “the same object?” |
|
Object contents | Business-logic equality |
|
Order (less/equal/greater) | Sorting, ordering |
8. Common mistakes when implementing equals and hashCode
Mistake #1: Overrode equals but forgot hashCode. Objects are considered equal but fall into different hash table buckets—lookup and removal break.
Mistake #2: You use mutable fields in hashCode. If a field changes after insertion into a collection, the object gets “lost”: the hash changes, but the bucket does not.
Mistake #3: Symmetry/transitivity of equals is violated. a.equals(b) returns true, but b.equals(a) is false, or transitivity breaks—collections start behaving unpredictably.
Mistake #4: You don’t check the class in equals. Comparing objects of different classes leads to incorrect results or exceptions.
Mistake #5: You compare strings and objects with ==. The == operator compares references; use equals for contents.
Mistake #6: No consistency between compareTo and equals. If a.compareTo(b) == 0 but !a.equals(b), TreeSet/TreeMap may treat elements as equal for ordering but different for equality—this is a source of “ghosts” and duplicates.
GO TO FULL VERSION