Skip to content
Ameen Abozaid
PublishedBEG-026-L03

The equals/hashCode contract

  • Lesson 3 of 6
  • 10 min read
  • Beginner
On This Lesson 11 topics

Lesson Overview

Lesson 2 ended with a decision: for a value such as Money, two objects with the same data should count as equal. In this lesson you make that happen by overriding equals, and you learn the rule that comes with it: whenever you override equals, you must override hashCode too, and the two must agree.

Back to the banknotes: a bank clerk who sorts notes into piles by value (hashCode) before checking two notes closely (equals) will only find a matching note if equal notes always land in the same pile. That is the whole contract in one picture.

Why this matters: hash-based collections, caches, and database-mapping frameworks all rely on this pair. Break the agreement and sets show duplicates, lookups fail, and nothing crashes to warn you. The next lessons and the lab show the failure with real output.

This lesson builds on Lessons 1 and 2, on instanceof with a pattern variable from the polymorphism module, and on final fields. Collections are not taught yet (a later module covers them); where one appears, treat it as a black box.

Learning Objectives

By the end of this lesson you will be able to:

  • List the five rules of the equals contract.
  • State the hashCode rule that links it to equals.
  • Write a correct equals and hashCode pair for a value class.
  • Use Objects.equals and Objects.hash to handle null fields and combine fields.
  • Choose which fields belong in the pair and explain why mutable ones are risky.
  • Explain why overriding only one of the two is a bug.

Written Explanation

The equals contract

When you override equals(Object), the Java documentation requires that it is an equivalence relation on non-null objects:

RuleMeaning
Reflexivex.equals(x) is true
Symmetricx.equals(y) is true exactly when y.equals(x) is
Transitiveif x.equals(y) and y.equals(z), then x.equals(z)
Consistentrepeated calls give the same answer if nothing used in the comparison changed
Non-nullx.equals(null) is false

Notice that the parameter type is Object, not your class. If you write equals(Money other), you have overloaded the method instead of overriding it, so @Override will give a compile error. Always write @Override and the Object parameter.

The hashCode contract

hashCode() returns an int. The documented rules:

  • Within one run, it returns the same value for the same object, as long as the information used in equals does not change.
  • If x.equals(y) is true, then x.hashCode() == y.hashCode() must be true.
  • If two objects are not equal, they are not required to have different hash codes. Different codes help performance (fewer clashes), but sharing one is allowed.

The reverse is not a rule: the same hash does not prove equality. A hash code is the pile number, not the identity card.

Why they travel together

Hash-based collections (such as HashSet and HashMap, which you will meet in a later module) work in two steps. First they use hashCode() to pick a bucket, a small group of entries. Then they use equals only on entries in that bucket. So if two equal objects produce different hash codes, they go to different buckets and the collection never even compares them. The result is silent duplicates and failed lookups. The default hashCode is identity-based, so a class that overrides only equals is exactly in this trap.

A reliable recipe

For a value class:

  1. Decide which fields define "the same value". (For Money: cents and currency. For a Customer identified by id: only id.)
  2. In equals: return true for the same object, return false if the other is not your type (this also handles null), then compare the chosen fields.
  3. In hashCode: combine the same fields.
java
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Money m)) return false;
    return cents == m.cents && currency.equals(m.currency);
}

@Override
public int hashCode() {
    return Objects.hash(cents, currency);
}

o instanceof Money m is false for null, so the non-null rule holds. Compare primitive fields with == and object fields with equals. If a field can be null, use Objects.equals(a, b) instead of a.equals(b).

Objects.hash(...) accepts any number of values and combines them; it copes with null values too. The exact numbers it produces are an implementation detail, so never hard-code them in tests.

Which fields should you use?

  • Use exactly the fields that define equality, in both methods.
  • Do not use fields that change after the object is stored in a hash-based collection. A hashCode based on a mutable field means the object is filed in one bucket, then the field changes, the hash changes, and later lookups search another bucket. The object seems to vanish, although it is still inside. The safest design is to base the pair on final fields, or on a stable identifier such as an id.
  • A class can be equal by id only. Two Customer objects with the same id and different names can be "the same customer at different moments", so you may choose to ignore the name.

A note on subclasses

Using instanceof in equals is fine when the class is final or when subclasses add no fields that matter. When subclasses add fields and are compared with their parents, symmetry can break. For now, keep value classes final (or immutable and simple) and you avoid the problem. Lesson 5 shows a way to skip this boilerplate altogether.

Examples

Example 1: Money with a matching pair (simple)

java
Money a = new Money(100, "SAR");
Money b = new Money(100, "SAR");
Money c = new Money(100, "SAR");
Money d = new Money(250, "SAR");
System.out.println(a.equals(a));
System.out.println(a.equals(b) + " " + b.equals(a));
System.out.println(a.equals(b) && b.equals(c) && a.equals(c));
System.out.println(a.equals(null));
System.out.println(a.equals("100 SAR"));
System.out.println(a.hashCode() == b.hashCode());
System.out.println(a.hashCode() + " " + d.hashCode());

Real output:

text
true
true true
true
false
false
true
85921 90571

Lines 1 to 4 are reflexive, symmetric, transitive, and non-null. A Money is never equal to a String. Equal objects share a hash code; the unequal one has a different number here, but it is allowed to share.

Example 2: Customer equal by id only (practical)

java
class Customer {
    private final long id;
    private String name;
    // constructor and rename(String) omitted
    @Override public boolean equals(Object o) {
        return this == o || (o instanceof Customer c && id == c.id);
    }
    @Override public int hashCode() { return Long.hashCode(id); }
}

Customer c1 = new Customer(7, "Ana");
Customer c2 = new Customer(7, "Ana Maria");
System.out.println(c1.equals(c2) + " " + (c1.hashCode() == c2.hashCode()));
c1.rename("A.");
System.out.println(c1.equals(c2));
text
true true
true

The name is mutable and not part of identity, so renaming does not change equals or hashCode. That is what makes it safe inside a hash-based collection. Using name in hashCode would not be safe.

Example 3: a field that may be null (realistic)

java
class Tag {
    private final String label;      // may be null
    Tag(String label) { this.label = label; }
    @Override public boolean equals(Object o) {
        return o instanceof Tag t && Objects.equals(label, t.label);
    }
    @Override public int hashCode() { return Objects.hashCode(label); }
}

System.out.println(new Tag(null).equals(new Tag(null)) + " " + new Tag(null).hashCode());
System.out.println(new Tag("x").equals(new Tag(null)));
text
true 0
false

label.equals(...) would throw for a null label. Objects.equals and Objects.hashCode are null-safe (Objects.hashCode(null) is 0). In real data, optional fields such as a middle name or a phone number are often null.

Backend Insight

Nearly every data class in a backend ends up in a collection, a cache key, or a framework that compares and hashes it. Typical places this pair matters:

  • Sets and maps of identifiers and values (covered in the collections module).
  • Cache keys: a cache stores an answer under a key object. If the key class lacks a correct pair, you get cache misses or, worse, duplicate work.
  • Entities loaded from a database: when the same row is loaded twice you often get two objects; the business meaning of "same row" is its id, so equals by id is common (you will see it again when mapping entities).

A design habit follows: keep key fields final, decide equality by a stable value, and write a small test: equal objects must have equal hash codes.

Common Mistakes

Mistake 1: Overriding equals without hashCode

The compiler does not complain. The code runs and sets contain duplicates. Lesson 6 shows it live. Always change both together.

Mistake 2: Mutable fields in hashCode

An object stored in a hash-based collection and then modified can no longer be found. Use final fields or an id.

Mistake 3: Using different fields in the two methods

If equals compares id but hashCode mixes in name, two equal objects can have different hash codes, which breaks the contract.

Mistake 4: Wrong signature

public boolean equals(Money m) overloads rather than overrides. @Override turns this into a compile error.

Mistake 5: Calling equals on a null field

name.equals(other.name) throws if name is null. Use Objects.equals.

Exercises

Try each exercise yourself before opening the solutions.

Level A: Foundation

A1. State the hashCode rule that links it to equals.

Show solution A1

A1. If a.equals(b) is true, then a.hashCode() must equal b.hashCode().

A2. Name three of the five rules of the equals contract.

Show solution A2

A2. Any three of: reflexive, symmetric, transitive, consistent, and x.equals(null) is false.

A3. True or false: two unequal objects must have different hash codes.

Show solution A3

A3. False. Unequal objects may share a hash code; distinct codes only help performance.

Level B: Application

B1. Write equals and hashCode for Book(String isbn, String title) where only isbn defines equality (assume isbn is never null).

Show solution B1

B1.

java
@Override public boolean equals(Object o) {
    return this == o || (o instanceof Book b && isbn.equals(b.isbn));
}
@Override public int hashCode() { return isbn.hashCode(); }

B2. This pair is broken. Explain why in one sentence and fix it.

java
@Override public boolean equals(Object o) {
    return o instanceof User u && id == u.id;
}
@Override public int hashCode() { return name.hashCode(); }
Show solution B2

B2. equals uses id but hashCode uses name, so two users with the same id and different names are equal yet have different hash codes. Fix: return Long.hashCode(id); (assuming id is a long), so both methods use the same field.

B3. What is wrong with public boolean equals(Book other)?

Show solution B3

B3. The parameter is Book, not Object, so it overloads instead of overriding. Collections and Object-typed calls will still use the inherited equals. Write @Override public boolean equals(Object o) and the compiler will catch the mistake.

Level C: Challenge

C1. A class Session(String token, long lastSeen) has a hashCode using both fields. lastSeen is updated on every request, and sessions are kept in a hash-based set. Describe the failure and give a fix.

Show solution C1

C1. The set files a session under a bucket from its hash at insertion time. When lastSeen changes, the hash changes, so contains searches another bucket and fails to find it, even though it is stored. Fix: base equals and hashCode on token only, which does not change.

C2. Write equals and hashCode for Address(String street, String city) when either field may be null.

Show solution C2

C2.

java
@Override public boolean equals(Object o) {
    return this == o || (o instanceof Address a
        && Objects.equals(street, a.street) && Objects.equals(city, a.city));
}
@Override public int hashCode() { return Objects.hash(street, city); }

Mini Challenge

Take a class Product(String sku, String name, long priceCents). Decide which fields define equality (give a reason), write the pair, and write a short main that checks: reflexive, symmetric, null, a different class, and equal hash codes for two equal products.

Check your work: you used the same fields in both methods, your main prints true, true, false, false, true, and the fields you used are final.

Summary & Next Lesson

Key takeaways

  • A correct equals is reflexive, symmetric, transitive, consistent, and false for null.
  • If two objects are equal, their hash codes must be equal. Unequal objects may share a hash code.
  • Hash-based collections use hashCode to find a bucket and equals inside it, so a mismatch gives silent duplicates.
  • Use the same, stable fields in both methods, and prefer final fields or an id.
  • Use Objects.equals and Objects.hash for null-safe code.

Next lesson

The next lesson, "toString", gives your objects a readable description for logs and debugging, and warns about leaking secrets.

Self-check

  1. If a.equals(b) is true, what must a.hashCode() and b.hashCode() satisfy?
  2. Why must equals take Object as its parameter type?
  3. Why is a mutable field risky in hashCode?
  4. Which method makes null fields safe when comparing?
  5. Can two unequal objects have the same hash code?
Show answers
  1. They must be equal.
  2. To override Object.equals; a different parameter type only overloads it.
  3. The hash changes after storage, so the object is looked up in the wrong bucket and appears lost.
  4. Objects.equals(a, b).
  5. Yes, it is allowed, though many clashes slow collections down.

References

  • Oracle, Java SE 21 API: java.lang.Object. It lists the equals rules (reflexive, symmetric, transitive, consistent, null) and the hashCode rules (consistent, equal objects have equal hash codes, unequal objects need not differ), and advises overriding hashCode with equals.
  • Oracle, Java SE 21 API: java.util.Objects. It covers Objects.equals, Objects.hash (computed as Arrays.hashCode over the values), and Objects.hashCode for null. Its note that Objects.hash(x) differs from Objects.hashCode(x) is why this lesson uses hashCode for the single-field case.
  • The bucket explanation is a simplified description of how hash-based collections work; that detail was not checked against the HashMap documentation in this lesson and the module on collections will cover it properly.
  • The numbers 85921 and 90571 come from running the examples on JDK 21 and are not guaranteed by the documentation.

Practice

Apply this lesson with the module's lab, exercises and quiz.

Lab and exercises Quiz

Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.

Continue to next lesson

Type to search levels, phases, modules and lessons.