CodeGym /Courses /JAVA 25 SELF /equals and hashCode contracts

equals and hashCode contracts

JAVA 25 SELF
Level 29, Lesson 0
Available

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?”
equals
Object contents Business-logic equality
compareTo
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.

1
Task
JAVA 25 SELF, level 29, lesson 0
Locked
Unique cities registry in a virtual world 🗺️
Unique cities registry in a virtual world 🗺️
1
Task
JAVA 25 SELF, level 29, lesson 0
Locked
Managing employees in the new HR system 👥
Managing employees in the new HR system 👥
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION