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.

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?

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.

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.

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.

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()) % 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.

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.

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.
All four scenarios of overriding equals() and hashCode()
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

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
Step 1 — Define the Class FieldsStart 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
Step 2 — Override equals() with Safety ChecksThe 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
Step 3 — Override hashCode() Using the Same FieldsThis 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
Step 4 — Verify the ContractCreate 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.

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.

Common pitfalls when overriding equals() and hashCode()
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
✦ 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.

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.

How this introductory lesson connects to more advanced Java topics
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()

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.

Practice Problems

PROBLEM 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?
PROBLEM 2 — BASIC CALCULATION
Given a Book class with fields String title and int isbn, write a correct equals() method. Include all necessary safety checks.
PROBLEM 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?
PROBLEM 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?
PROBLEM 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?

Lesson 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)