Java • Object-Oriented Programming

Overriding equals() & hashCode() — Override equals() and hashCode() consistently (intro)

Learn why Java objects need custom equality logic and how to keep equals() and hashCode() in sync.

Java programming emblem with a coffee cup, code braces, and terminal
Inside this lesson 10 sections 5 questions 6 figures

SECTION 1

Historical Context & Motivation

When Java was first released in 1995, its designers made a critical decision: every single object would inherit a method called equals() and another called hashCode() from the root class Object. The default behavior of equals() simply checks whether two references point to the exact same spot in memory — the same physical object. This works fine for some situations, but it completely breaks down when you want to compare the meaningful content of two separate objects. For example, two Student objects with the same name and ID should probably be considered "equal," even though they live at different memory addresses.

As Java matured and developers started using collections like HashMap and HashSet, the relationship between equals() and hashCode() became essential. Hash-based collections rely on hash codes to quickly locate objects, and if the two methods disagree, collections silently malfunction. The problem of keeping these two methods consistent has been a central topic in Java programming since the language's earliest days.

1995 • Java 1.0 Released

The Object class ships with default equals() and hashCode() methods based on memory identity.

1998 • Java Collections Framework

Java 1.2 introduces HashMap, HashSet, and other hash-based collections, making consistent overriding of both methods critically important.

2001 • Effective Java Published

Joshua Bloch's influential book formalizes the rules for overriding equals() and hashCode() — the "contract" that all Java developers should follow.

2014+ • IDE Auto-Generation

Modern IDEs like IntelliJ IDEA and Eclipse offer auto-generation of both methods, reducing bugs but not eliminating the need to understand the underlying rules.

The core question this lesson addresses is straightforward: How do you teach Java to compare your custom objects by their content rather than their memory address, and why must you always update both equals() and hashCode() together?

SECTION 2

Core Principles & Definitions

Before you start writing code, you need to understand the foundational ideas behind object equality in Java. There are two very different kinds of equality, and confusing them is one of the most common beginner mistakes. Let's define the core concepts and the rules — known as the equals/hashCode contract — that govern how these methods must behave.

1

Reference Equality (==)

The == operator checks if two variables point to the same object in memory. Two different objects with identical data will return false.
2

Logical Equality (equals())

The equals() method checks if two objects are meaningfully equivalent based on their fields. You define what "meaningful" means when you override it.
3

hashCode() — The Bucket Index

The hashCode() method returns an integer used by hash-based collections (like HashMap) to quickly find and organize objects into "buckets."
4

The Contract

If a.equals(b) returns true, then a.hashCode() must equal b.hashCode(). Violating this rule causes collections to lose track of your objects.
5

Properties of equals()

A correct equals() must be: reflexive (x.equals(x) is true), symmetric (x.equals(y) ↔ y.equals(x)), transitive, and consistent.
✦

KEY TAKEAWAY

Think of equals() and hashCode() like a school's filing system. The hash code is the folder label (e.g., "Last names A–C") that tells you which drawer to look in. The equals method is like reading the actual student record to confirm you found the right person. If two identical records are filed in different drawers (different hash codes), you'll never find the second copy — even though it's a perfect match.

SECTION 3

Visual Explanation — Reference vs. Logical Equality

The diagram below illustrates the fundamental difference between reference equality (the == operator) and logical equality (a properly overridden equals() method). Pay close attention to the memory addresses versus the data stored inside each object.

Reference Equality (==) vs. Logical Equality (equals())HEAP MEMORYAddress: 0x3A7FStudent {name = "Alice"id = 1001}Address: 0x8B12Student {name = "Alice"id = 1001}STACK (Variables)s1 → 0x3A7Fs2 → 0x8B12s1 == s2false ✗Different memory addressess1.equals(s2)true ✓Same name & id (if overridden)
Variables s1 and s2 point to different memory addresses (0x3A7F vs. 0x8B12), so == returns false. However, a properly overridden equals() compares the fields and returns true because both have the same name and id.

Notice how s1 and s2 contain identical data — both represent a student named Alice with ID 1001. The == operator doesn't care about that; it only checks the memory address, which is different for each object. This is why you need to override equals() whenever you want to compare objects by their content.

SECTION 4

How equals() and hashCode() Work Together

Hash-based collections like HashMap and HashSet use a two-step process to store and find objects. First, they call hashCode() to determine which "bucket" to look in. Then, they call equals() on each item in that bucket to find the exact match. Understanding this two-step lookup is the key to understanding why both methods must stay consistent.

The hashCode() Lookup Process

BUCKET INDEX FORMULA

bucketIndex=Math.abs(obj.hashCode())bucketIndex = Math.abs(obj.hashCode()) % numberOfBuckets

Where obj.hashCode() returns an integer, and numberOfBuckets is the internal array size of the collection. The modulo (%) operator ensures the result fits within the array bounds.

The equals/hashCode Contract (Formal Rules)

  1. Rule 1: If a.equals(b) is true, then a.hashCode() == b.hashCode() must also be true.
  2. Rule 2: If a.hashCode() != b.hashCode(), then a.equals(b) must be false. (This is the logical contrapositive of Rule 1.)
  3. Rule 3: Two unequal objects may have the same hash code. This is called a hash collision — it's allowed but reduces performance.
⚠

Common Pitfall

If you override equals() but forget to override hashCode(), two logically equal objects may land in different buckets. A HashSet could then contain two "equal" objects — a sneaky bug that is very hard to track down.

SECTION 5

Detailed Breakdown — How Hash Buckets Use Both Methods

The diagram below shows what happens inside a HashSet when you add and search for objects. It traces the path from calling hashCode() to locate a bucket, then calling equals() to find or confirm the exact object within that bucket. This visual makes clear why breaking the contract causes objects to "vanish" from collections.

HashSet Internal Lookup: hashCode() → Bucket → equals()Step 1obj.hashCode() → 42Step 242 % 8 = bucket[2]Step 3for each in bucket: equals()?Bucket Array (size = 8)[0][1][2] ←[3][4][5][6][7]Items in Bucket [2]Student{"Bob", 2002}equals(obj)? → false ✗Student{"Alice", 1001}equals(obj)? → true ✓ FOUND!⚠ What if hashCode() wasn't overridden?Default hashCode uses memory address → our new Student{"Alice", 1001}would get a DIFFERENT hash code → lands in DIFFERENT bucket→ equals() never even gets called → object is "lost"!
A HashSet first computes the bucket index from hashCode(), then walks through the bucket comparing each item with equals(). The red warning box shows what goes wrong when hashCode() isn't overridden — the object ends up in the wrong bucket and can never be found.
Scenarioequals() Overridden?hashCode() Overridden?HashSet Behavior
Neither overriddenNoNoUses memory identity — duplicates allowed if different objects
Only equals() overriddenYesNoBROKEN — equal objects land in different buckets
Only hashCode() overriddenNoYesSame bucket, but equals() still uses memory identity — duplicates allowed
Both overridden consistentlyYesYesCORRECT — logically equal objects are treated as duplicates

All four scenarios of overriding equals() and hashCode()

SECTION 6

Worked Example — Writing a Student Class

Let's build a complete Student class that properly overrides both equals() and hashCode(). We'll define two students as equal if they have the same name and id.

Overriding equals() and hashCode() in a Student Class

  1. 1

    Step 1 — Define the Class Fields

    Start with a simple class that has two fields. We consider two Student objects equal if both name and id match.
    public class Student { private String name; private int id; }
  2. 2

    Step 2 — Override equals() with Safety Checks

    The method signature must be public boolean equals(Object obj) — taking Object as the parameter, not Student. First, check if the references are identical (this == obj). Then check for null and ensure the object is the right type using instanceof. Finally, cast and compare the fields.
    @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || !(obj instanceof Student)) return false; Student other = (Student) obj; return this.id == other.id && this.name.equals(other.name); }
  3. 3

    Step 3 — Override hashCode() Using the Same Fields

    This is the critical part: you must use the same fields in hashCode() that you used in equals(). Java provides a convenient helper: Objects.hash(). Alternatively, you can manually combine values using a prime multiplier like 31.
    @Override public int hashCode() { return Objects.hash(name, id); }
  4. 4

    Step 4 — Verify the Contract

    Create two Student objects with the same data and test that equals() returns true and both hashCode() values are identical. Then add both to a HashSet — it should contain only one entry.
    Student s1 = new Student("Alice", 1001); Student s2 = new Student("Alice", 1001); s1.equals(s2) → true; s1.hashCode() == s2.hashCode() → true; Set<Student> set = new HashSet<>(); set.add(s1); set.add(s2); set.size() → 1
💡

Pro Tip: @Override Annotation

Always use the @Override annotation. If you accidentally write equals(Student obj) instead of equals(Object obj), you're creating a new overloaded method — not overriding the one from Object. The @Override annotation catches this mistake at compile time.

SECTION 7

Common Pitfalls & Best Practices

Overriding these methods seems straightforward, but there are several traps that catch even experienced programmers. The table below summarizes the most common mistakes alongside the correct approach.

PitfallWhat Goes WrongBest Practice
Override equals() but not hashCode()Equal objects get different hash codes → lost in HashMap/HashSetAlways override both together
Wrong parameter type: equals(Student)Creates an overload, not an override — collections call equals(Object)Use equals(Object obj) with @Override
Using mutable fields in hashCode()If a field changes after insertion, the hash code changes → object is unreachablePrefer immutable fields for equality
Not handling null in equals()NullPointerException at runtimeCheck for null before casting
Using different fields in equals() vs. hashCode()Contract violated — equal objects may have different hash codesUse the exact same fields in both methods

Common pitfalls when overriding equals() and hashCode()

✦

KEY TAKEAWAY

Think of overriding equals() without hashCode() like changing your phone number but not updating it in your contacts app. When someone searches for you by name (equals), the app looks up the old number (hashCode) first, doesn't find your entry, and concludes you don't exist — even though you're right there in the database.

SECTION 8

Connection to Advanced Topics

Once you understand the basics of equals() and hashCode(), there are several advanced topics that build on this foundation. Java's ecosystem has evolved to provide tools and patterns that make overriding these methods easier, and understanding the contract is essential for working with more sophisticated data structures.

This Lesson (Intro)Advanced Topic
Manually writing equals() and hashCode()Using Objects.equals(), Objects.hash(), or IDE auto-generation for reliability
Flat class with primitive and String fieldsInheritance hierarchies — equals() symmetry breaks when subclasses add fields
Using fields directly in hashCode()Caching hash codes for immutable objects to improve performance
HashSet and HashMap basicsUnderstanding TreeMap/TreeSet with Comparable and compareTo() consistency
Writing equals()/hashCode() by handJava Records (Java 14+) auto-generate correct equals() and hashCode()

How this introductory lesson connects to more advanced Java topics

In future lessons, you'll see how Java Records — introduced in Java 14 — automatically generate correct equals() and hashCode() implementations. You'll also explore the tricky problem of maintaining the equals/hashCode contract across inheritance hierarchies. For now, mastering the manual approach gives you the strongest foundation.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

Explain in your own words why overriding equals() without overriding hashCode() can cause objects to be "lost" in a HashSet. What specific step in the lookup process fails?
Show answer
When you override only equals(), two logically equal objects may still have different hash codes (because the default hashCode() uses memory addresses). A HashSet computes the bucket index from the hash code first. Since the hash codes differ, the two equal objects land in different buckets. Step 3 of the lookup — comparing with equals() — never even happens because the set is searching in the wrong bucket. The object appears to vanish.

Question 2: BASIC CALCULATION

Given a Book class with fields String title and int isbn, write a correct equals() method. Include all necessary safety checks.
Show answer
@Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || !(obj instanceof Book)) return false; Book other = (Book) obj; return this.isbn == other.isbn && this.title.equals(other.title); } The method first checks reference equality for efficiency, then checks for null and type compatibility. After casting, it compares both fields. We use == for the primitive int and .equals() for the String.

Question 3: INTERMEDIATE

A programmer writes the following equals() method for a Point class: public boolean equals(Point p) { return this.x == p.x && this.y == p.y; } They also write a matching hashCode(). When they add two equal Point objects to a HashSet, the set reports size 2 instead of 1. What went wrong, and how do you fix it?
Show answer
The parameter type is Point instead of Object. This creates an overloaded method, not an override. Collections internally call equals(Object), which still uses the default memory-address comparison from Object. The fix is to change the parameter to Object obj, add the @Override annotation (which would have caught this at compile time), check for null and instanceof, then cast to Point before comparing fields.

Question 4: APPLIED

You're building an online store. A Product class has fields: String sku, String name, and double price. You want a shopping cart (implemented as a HashMap<Product, Integer>) to recognize two Product objects with the same sku as the same product. Write both equals() and hashCode(). Should you include price in the equality check? Why or why not?
Show answer
You should base equality only on sku because the SKU is the unique product identifier. The price may change (e.g., a sale), and if price is part of equals/hashCode, changing the price would alter the hash code of a product already in the map, making it unreachable. @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || !(obj instanceof Product)) return false; Product other = (Product) obj; return this.sku.equals(other.sku); } @Override public int hashCode() { return Objects.hash(sku); } This keeps the contract consistent and avoids mutable-field issues.

Question 5: CRITICAL THINKING

Consider a hashCode() method that always returns 42 for every object, regardless of fields. Does this violate the equals/hashCode contract? What practical consequence would it have for a HashMap containing 10,000 entries?
Show answer
Technically, returning the same hash code for every object does NOT violate the contract. The contract only requires that equal objects have equal hash codes — it says nothing about unequal objects needing different hash codes. However, the practical consequence is devastating: every object lands in the same bucket. A HashMap with 10,000 entries would degenerate into a single linked list. Lookups that should be O(1) become O(n), meaning searches take up to 10,000 comparisons instead of roughly 1. The code is correct but extremely slow — a valid but terrible implementation.

SUMMARY

Lesson Summary

Summary

Java's equals() method determines logical equality — whether two objects are meaningfully the same based on their fields — while hashCode() returns an integer that hash-based collections use to organize objects into buckets. The equals/hashCode contract states that if a.equals(b) is true, then a.hashCode() == b.hashCode() must also be true. Violating this contract causes objects to become unreachable in HashMap and HashSet.

When overriding, always use the same fields in both methods, use the @Override annotation to catch signature errors, and include null and type checks in equals(). A correct equals() must be reflexive, symmetric, transitive, and consistent. Prefer immutable fields to avoid the problem of objects "moving" between buckets when field values change after insertion.

Varsity Tutors • Java • Overriding equals() & hashCode() — Override equals() and hashCode() consistently (intro)