equals(), hashCode() & Object Methods

Reviewed & published by Brayan K

equals() defines when two objects count as "the same value", and hashCode() must agree with it — together they are the contract every object stored in a HashSet or HashMap has to honour.

Part of the free Java course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

Before You Start

You should know classes and objects and have used the Collections (especially HashSet and HashMap). A glance at records shows the shortcut that generates these for you.

What You'll Learn

The Big Idea — Library Shelves

💡 Analogy: A HashMap is a library where hashCode is the shelf number and equals is reading the title to find the exact book. To return a book you first walk to the shelf its hash points to, then check titles on that shelf. If two identical books somehow get different shelf numbers (equal by equals but different hashCode), the librarian walks to the wrong shelf and swears the book isn't there. That is the silent bug behind "my object is in the set but contains() returns false".

The rule that keeps the library sane: equal objects must hash to the same shelf. Override one, override the other.

1️⃣ The Default Behaviour (identity)

Every class inherits equals and hashCode from Object. By default equals is just == (reference identity) and hashCode is derived from the object's memory identity. So two objects with identical fields are not equal, and a HashSet happily stores both as duplicates.

import java.util.*;

public class Main {
    // A class with NO equals/hashCode override — uses Object's identity.
    static class PointBad {
        final int x, y;
        PointBad(int x, int y) { this.x = x; this.y = y; }
    }

    public static void main(String[] args) {
        PointBad a = new PointBad(1, 2);
        PointBad b = new PointBad(1, 2);

        System.out.println("a == b        : " + (a == b));      // different objects
        System.out.println("a.equals(b)   : " + a.equals(b));   // default: identity!
        Set<PointBad> set = new HashSet<>();
        set.add(a);
        System.out.println("set contains b: " + set.contains(b)); // false — bug!
        set.add(b);
        System.out.println("set size       : " + set.size());   // 2 — duplicates!
    }
}

2️⃣ Implementing equals and hashCode Correctly

A correct equals follows a fixed shape: identity fast-path, an instanceof check (which also rejects null), a cast, then a field-by-field comparison. The matching hashCode uses Objects.hash(...) over the same fields. Add a readable toString while you're there.

@Override public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Point)) return false;
    Point p = (Point) o;
    return x == p.x && y == p.y;
}
@Override public int hashCode() { return Objects.hash(x, y); }
import java.util.*;

public class Main {
    // A correctly implemented value class.
    static final class Point {
        final int x, y;
        Point(int x, int y) { this.x = x; this.y = y; }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;                 // fast path
            if (!(o instanceof Point)) return false;    // null + type check
            Point p = (Point) o;
            return x == p.x && y == p.y;                 // compare fields
        }

        @Override
        public int hashCode() {
            return Objects.hash(x, y);                   // agree with equals
        }

        @Override
        public String toString() {
            return "Point(" + x + ", " + y + ")";
        }
    }

    public static void main(String[] args) {
        Point a = new Point(1, 2);
        Point b = new Point(1, 2);
        Point c = new Point(3, 4);

        System.out.println("a.equals(b)      : " + a.equals(b));
        System.out.println("a.equals(c)      : " + a.equals(c));
        System.out.println("hashCodes match  : " + (a.hashCode() == b.hashCode()));

        Set<Point> set = new HashSet<>();
        set.add(a);
        set.add(b);                                      // duplicate of a
        set.add(c);
        System.out.println("set size          : " + set.size());   // 2
        System.out.println("set contains (1,2): " + set.contains(new Point(1, 2)));
        System.out.println("toString          : " + a);

        Map<Point, String> labels = new HashMap<>();
        labels.put(a, "origin-ish");
        System.out.println("map lookup via b  : " + labels.get(b));
    }
}

3️⃣ The Classic Bug — equals without hashCode

Override equals but forget hashCode and you get the most infamous Java bug: objects that are equal but have different hash codes. They land in different buckets, so set.contains and map.get fail to find them even though an equal object is right there.

4️⃣ Records Give You All Three for Free

Declare the same value type as a record and the compiler generates equals, hashCode, and toString — all consistent, all derived from the components. For simple immutable data carriers this is the safest and shortest option.

import java.util.*;

public class Main {
    // A record gives you equals, hashCode and toString FOR FREE.
    record Money(String currency, int cents) {}

    public static void main(String[] args) {
        Money a = new Money("USD", 500);
        Money b = new Money("USD", 500);

        System.out.println("a.equals(b)        : " + a.equals(b));   // true
        System.out.println("hashCodes equal?   : " + (a.hashCode() == b.hashCode())); // true
        System.out.println("toString           : " + a);

        Set<Money> set = new HashSet<>();
        set.add(a);
        System.out.println("set.contains(b)?   : " + set.contains(b)); // true — works!
    }
}

🧠 Quick Recall

Predict before you peek.

Answer: illegal — it breaks the contract and breaks hash collections. The reverse (different objects sharing a hashCode) is legal; that's just a collision, which equals resolves.

Answer: because a might be null. Objects.equals(a, b) is null-safe — it returns true if both are null and never throws an NPE.

Answer: no — == compares references and new String makes a distinct object. .equals is true. Always use .equals for String value comparison.

🎯 YOUR TURN — Equal by ISBN

Override equals and hashCode on Book so two books with the same ISBN are one in a HashSet.

import java.util.*;

public class Main {
    static final class Book {
        final String isbn;
        Book(String isbn) { this.isbn = isbn; }

        // YOUR TURN: override equals so two Books are equal when their isbn
        // matches (use Objects.equals), and override hashCode with
        // Objects.hash(isbn) so they AGREE. Then the HashSet below should
        // treat duplicate ISBNs as one.
        //
        // Expected output:
        //   Distinct books: 2
        //   Has 978-0? true
    }

    public static void main(String[] args) {
        Set<Book> shelf = new HashSet<>();
        shelf.add(new Book("978-0"));
        shelf.add(new Book("978-0"));
        shelf.add(new Book("978-1"));
        System.out.println("Distinct books: " + shelf.size());
        System.out.println("Has 978-0? " + shelf.contains(new Book("978-0")));
    }
}

🧩 MINI-CHALLENGE — Record as a map key

Use a record Coord(int x, int y) as a HashMap key. Re-putting the same coordinate overwrites, and lookups by a fresh Coord must hit.

Common Errors

Pro Tips

📋 Quick Reference

ConceptSyntaxNote
identity fast pathif (this == o) return true;First line of equals
type/null checkif (!(o instanceof T)) return false;false for null too
field compareObjects.equals(a, b)Null-safe
primitive comparex == p.x== fine for primitives
hashCodeObjects.hash(f1, f2)Same fields as equals
contractequal ⇒ same hashCodeReverse not required
override marker@OverrideCatches bad signatures
toString@Override String toString()Readable output
value vs identity.equals vs ==Value vs reference
recordrecord T(...)All three for free
use inHashSet / HashMapNeeds correct pair

📚 Resources / Keep Learning

🎉 Lesson Complete!

You now know what the default equals/hashCode do, the contract that binds them, how to write both correctly with instanceof, Objects.equals and Objects.hash, why a missing hashCode silently breaks hash collections, and how records hand you all of it for free.

Practice quiz

What does the default Object.equals compare?

  • The fields of the objects
  • Object identity (same reference)
  • The toString output
  • The class names

Answer: Object identity (same reference). Without an override, equals returns true only when both references point to the very same object (==).

The equals/hashCode contract requires that...

  • equal objects have equal hashCodes
  • unequal objects have unequal hashCodes
  • hashCode is always 0
  • equals is faster than hashCode

Answer: equal objects have equal hashCodes. If a.equals(b) is true then a.hashCode() must equal b.hashCode(). The reverse is not required — collisions are allowed.

What breaks if you override equals but NOT hashCode?

  • Nothing
  • HashSet/HashMap lookups fail to find equal objects
  • Compilation fails
  • toString breaks

Answer: HashSet/HashMap lookups fail to find equal objects. Hash-based collections use hashCode to pick a bucket; if equal objects have different hashCodes they land in different buckets and can't be found.

Which helper computes a hash from several fields?

  • Objects.hash(x, y)
  • Objects.equals(x, y)
  • Math.hash(x, y)
  • Arrays.hash(x, y)

Answer: Objects.hash(x, y). Objects.hash(field1, field2, ...) returns a combined hash and handles nulls for you.

Which helper null-safely compares two fields in equals?

  • a == b
  • a.equals(b)
  • Objects.equals(a, b)
  • Objects.compare(a, b)

Answer: Objects.equals(a, b). Objects.equals(a, b) returns true if both are null or a.equals(b), avoiding a NullPointerException.

Do records generate equals and hashCode for you?

  • No, you must write them
  • Yes, based on all components
  • Only equals
  • Only hashCode

Answer: Yes, based on all components. A record automatically gets equals, hashCode and toString derived from its components — no boilerplate.

A correct equals should first check...

  • this == o for a fast path
  • the toString
  • the hashCode
  • the array length

Answer: this == o for a fast path. if (this == o) return true; is a quick identity short-circuit before the more expensive field comparison.

How should equals handle a null or wrong-type argument?

  • Throw an exception
  • Return true
  • Return false (e.g. via instanceof)
  • Ignore it

Answer: Return false (e.g. via instanceof). instanceof returns false for null and for the wrong type, so 'if (!(o instanceof T)) return false;' handles both safely.

What is a key property of equals?

  • It must be symmetric: a.equals(b) == b.equals(a)
  • It must be random
  • It must compare hashCodes
  • It must be slower than hashCode

Answer: It must be symmetric: a.equals(b) == b.equals(a). equals must be reflexive, symmetric, transitive, and consistent — symmetry being a classic one people break with inheritance.

What does Object.toString return by default?

  • The field values
  • ClassName@hexHashcode
  • An empty string
  • null

Answer: ClassName@hexHashcode. The default is getClass().getName() + '@' + Integer.toHexString(hashCode()) — rarely useful, so override it.

Continue this course

Frequently asked questions

Why must equals and hashCode always be overridden together?

Because hash-based collections (HashSet, HashMap, HashTable) rely on both. They first use hashCode to choose a bucket, then equals to compare within that bucket. The contract says: if two objects are equal, they MUST have the same hashCode. If you override equals but leave the default hashCode, two 'equal' objects almost always land in different buckets, so a HashSet stores both as duplicates and contains/get fail to find them. The two methods must tell a consistent story.

What exactly is the equals contract?

equals must be reflexive (x.equals(x) is true), symmetric (x.equals(y) iff y.equals(x)), transitive (if x equals y and y equals z, then x equals z), consistent (repeated calls give the same result while the objects don't change), and x.equals(null) must be false. Breaking symmetry is the most common slip, usually when comparing across a parent and a subclass — which is one reason value types are often final.

How do Objects.equals and Objects.hash help?

Objects.equals(a, b) is a null-safe equality check: it returns true if both are null, false if exactly one is null, and otherwise a.equals(b) — so you never get a NullPointerException on a possibly-null field. Objects.hash(f1, f2, ...) combines any number of fields into one hash code, also handling nulls. Together they let you write correct equals/hashCode in a couple of clean lines instead of fiddly manual logic.

Do I still need to write equals and hashCode if I use records?

No. A record automatically generates equals, hashCode, and toString from its components, all consistent with each other and with the canonical constructor. That is one of the best reasons to use a record for a simple immutable data carrier — it eliminates exactly the boilerplate that is so easy to get wrong. Only write them by hand if you need custom semantics (for example, comparing only some fields).

Should I use == or equals to compare objects?

Use equals for logical/value equality ('do these represent the same thing?') and == for identity ('are these the very same object?'). For objects, == compares references, so two distinct objects with identical contents are not == . Strings are the classic trap: "a" == new String("a") is false, but .equals is true. Use == only for primitives and for genuine identity checks (often inside equals itself as a fast path, or with null).

Related lessons