> ## Content Index
> Fetch the complete content index at: https://www.ggorantala.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# How to Implement equals() and hashCode() in Java (Java 21)
- URL: https://www.ggorantala.dev/how-to-implement-equals-hashcode-java/
- Published: 2026-04-30T04:52:00.000Z
- Updated: 2026-10-31T06:35:11.000Z
- Description: Write equals() and hashCode() you can trust: a parcel tracker built with plain Java 21, four classic bugs forced on purpose, and 16 checks that prove the fix.
- Author: Gopi Gorantala
- Tags: how-to-tutorials, core-java, java-tutorials

## Start with the finished app

Here's the output of the program you'll have at the end. Three of the five sections are bugs I forced on purpose. The fourth is the class that fixes them.

```log
--- 1. equals without hashCode ---
a.equals(b): true
set contains an equal parcel? false
set size after adding an 'equal' one: 2
--- 2. mutable key ---
before change: In transit
after change:  null
map still holds it: 1 entry, containsKey=false
--- 3. subclass breaks symmetry ---
plain.equals(express): true
express.equals(plain): false
--- 4. the fixed Parcel ---
a.equals(b): true
hash codes equal: true
set contains b: true
after reweigh, set still contains a: true
different carrier equal? false
equals(null): false
--- 5. record ---
true

```

Read the first block twice. `a.equals(b)` is `true`, and the `HashSet` still says it doesn't contain an equal parcel. That's the most common `equals()` and `hashCode()` bug in Java, and it doesn't throw anything. The parcel just vanishes.

This is for you if you've written `equals` before and weren't sure it was right, or if you've let the IDE generate it and never looked at the result. By the end you'll know how to implement equals and hashCode in Java so that `HashMap`, `HashSet` and `List.contains` all agree, and you'll have 16 runnable checks to copy into your own project.

The domain is a parcel tracker. A parcel is identified by its carrier and tracking number; its weight and recipient can change. That split, identity versus state, is the whole design decision.

## Versions I used

| Tool       | Version I used                   | Check it with  |
| ---------- | -------------------------------- | -------------- |
| JDK        | OpenJDK 21.0.12.1 (Ubuntu build) | java -version  |
| javac      | 21.0.12.1                        | javac -version |
| Build tool | none, plain javac                |                |

No libraries, no framework, nothing to download. Anything from Java 16 up will compile the code as written (it uses `instanceof` pattern matching and a record). On Java 11 or older, replace the pattern variables with a cast.

I built and ran everything from the command line this run. IntelliJ IDEA and Eclipse steps below come from each IDE's documentation; I didn't click through them. The whole thing took me about 40 minutes, and most of that was provoking the bugs in a way that printed something clear rather than crashed.

You should already know what a `HashMap` is and what `==` does on objects. That's it.

## Create the project

The primary path here is the command line. Make a folder and the package directories:

```bash
mkdir -p parcels/src/main/java/com/example/parcels
cd parcels

```

The finished tree:

```log
parcels
└── src
    └── main
        └── java
            └── com
                └── example
                    └── parcels
                        ├── App.java
                        ├── BrokenParcel.java
                        ├── ExpressParcel.java
                        ├── LooseParcel.java
                        ├── MutableKeyParcel.java
                        ├── Parcel.java
                        ├── ParcelChecks.java
                        └── ParcelRecord.java

```

**In IntelliJ IDEA:** File → New → Project, choose Java as the generator, pick a JDK 21, and leave the build system on IntelliJ. Right-click `src` → New → Package → `com.example.parcels`, then add each class below. IDEA can generate `equals` and `hashCode` through Code → Generate → equals() and hashCode(); I'll say what to watch for in that wizard later.

**In Eclipse:** File → New → Java Project, select a JavaSE-21 execution environment, and create the package under `src`. Eclipse's version of the generator is Source → Generate hashCode() and equals().

**With a build tool:** The code has no dependencies, so any `mvn archetype:generate` or `gradle init` skeleton works. Put the files under `src/main/java`.

## Write the code

Two rules before the code. First, `equals` and `hashCode` are one contract, and you change them together or not at all. Second, use only fields that define identity. Everything else stays out.

### Step 1: Write the class that has equals() but no hashCode()

Goal: see the bug first, so you recognise it later.

`src/main/java/com/example/parcels/BrokenParcel.java`

```java
package com.example.parcels;

import java.util.Objects;

/** equals() without hashCode(), and a mutable field that takes part in both. */
public class BrokenParcel {

    private String trackingNumber;

    public BrokenParcel(String trackingNumber) {
        this.trackingNumber = trackingNumber;
    }

    public String getTrackingNumber() {
        return trackingNumber;
    }

    public void setTrackingNumber(String trackingNumber) {
        this.trackingNumber = trackingNumber;
    }

    @Override
    public boolean equals(Object o) {
        return o instanceof BrokenParcel other
                && Objects.equals(trackingNumber, other.trackingNumber);
    }
    // no hashCode(): the bug

    @Override
    public String toString() {
        return "BrokenParcel[" + trackingNumber + "]";
    }
}

```

Why it matters: `equals` says two parcels with the same tracking number are the same. But `HashSet` and `HashMap` don't call `equals` first. They call `hashCode`, pick a bucket, and only then compare with `equals` inside that bucket. Without an override, `hashCode` comes from `Object` and is based on identity, so two equal parcels land in different buckets and never meet.

Checkpoint: compile it.

```bash
javac -d out src/main/java/com/example/parcels/BrokenParcel.java

```

No output means success.

### Step 2: Write the class whose hash code can change

Goal: show that a correct pair still fails if the fields it reads can change.

`src/main/java/com/example/parcels/MutableKeyParcel.java`

```java
package com.example.parcels;

import java.util.Objects;

/** equals() and hashCode() agree, but the fields they use can change. */
public class MutableKeyParcel {

    private String trackingNumber;

    public MutableKeyParcel(String trackingNumber) {
        this.trackingNumber = trackingNumber;
    }

    public void setTrackingNumber(String trackingNumber) {
        this.trackingNumber = trackingNumber;
    }

    @Override
    public boolean equals(Object o) {
        return o instanceof MutableKeyParcel other
                && Objects.equals(trackingNumber, other.trackingNumber);
    }

    @Override
    public int hashCode() {
        return Objects.hash(trackingNumber);
    }

    @Override
    public String toString() {
        return "MutableKeyParcel[" + trackingNumber + "]";
    }
}

```

The pair here is right. `hashCode` uses the same single field as `equals`. The problem is `setTrackingNumber`. If you put one of these in a `HashSet` or use it as a `HashMap` key and then change the number, the object sits in a bucket chosen by its old hash.

### Step 3: Write the subclass that breaks symmetry

Goal: see why `instanceof` in `equals` and a subclass that adds state don't mix.

`src/main/java/com/example/parcels/LooseParcel.java`

```java
package com.example.parcels;

import java.util.Objects;

/** Uses instanceof in equals on a non-final class. Fine until someone subclasses it. */
public class LooseParcel {

    protected final String trackingNumber;

    public LooseParcel(String trackingNumber) {
        this.trackingNumber = trackingNumber;
    }

    @Override
    public boolean equals(Object o) {
        return o instanceof LooseParcel other
                && Objects.equals(trackingNumber, other.trackingNumber);
    }

    @Override
    public int hashCode() {
        return Objects.hash(trackingNumber);
    }
}

```

`src/main/java/com/example/parcels/ExpressParcel.java`

```java
package com.example.parcels;

import java.util.Objects;

/** Adds a field and extends equals to include it. That breaks symmetry. */
public class ExpressParcel extends LooseParcel {

    private final String deliveryWindow;

    public ExpressParcel(String trackingNumber, String deliveryWindow) {
        super(trackingNumber);
        this.deliveryWindow = deliveryWindow;
    }

    @Override
    public boolean equals(Object o) {
        return o instanceof ExpressParcel other
                && super.equals(other)
                && Objects.equals(deliveryWindow, other.deliveryWindow);
    }

    @Override
    public int hashCode() {
        return Objects.hash(trackingNumber, deliveryWindow);
    }
}

```

`ExpressParcel` does what most of us would do: call `super.equals`, then compare the new field. The catch is that `plain.equals(express)` only asks the `LooseParcel` rules, so it says yes, and `express.equals(plain)` fails the `instanceof ExpressParcel` test. One answer one way, a different one the other.

### Step 4: Write the Parcel that gets it right

Goal: the class you'd actually ship.

`src/main/java/com/example/parcels/Parcel.java`

```java
package com.example.parcels;

import java.util.Objects;

/**
 * A parcel is identified by its carrier and tracking number.
 * Weight and recipient can change over the parcel's life, so they stay out of equals/hashCode.
 */
public class Parcel {

    private final String carrier;
    private final String trackingNumber;
    private String recipient;   // mutable, not part of identity
    private int weightGrams;    // mutable, not part of identity

    public Parcel(String carrier, String trackingNumber, String recipient, int weightGrams) {
        this.carrier = Objects.requireNonNull(carrier, "carrier");
        this.trackingNumber = Objects.requireNonNull(trackingNumber, "trackingNumber").trim().toUpperCase();
        this.recipient = recipient;
        this.weightGrams = weightGrams;
    }

    public String carrier() { return carrier; }
    public String trackingNumber() { return trackingNumber; }
    public String recipient() { return recipient; }
    public int weightGrams() { return weightGrams; }
    public void reweigh(int grams) { this.weightGrams = grams; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;                       // fast path
        if (o == null || getClass() != o.getClass()) return false;
        Parcel other = (Parcel) o;
        return carrier.equals(other.carrier)
                && trackingNumber.equals(other.trackingNumber);
    }

    @Override
    public int hashCode() {
        return Objects.hash(carrier, trackingNumber);
    }

    @Override
    public String toString() {
        return "Parcel[" + carrier + ":" + trackingNumber + ", " + weightGrams + "g]";
    }
}

```

Decisions worth checking line by line:

- `carrier` and `trackingNumber` are `final`. They define identity, so they can't change after construction. That closes the mutable-key hole from Step 2 without any extra code.
- `recipient` and `weightGrams` change over a parcel's life, so they stay out of both methods. Reweighing a parcel doesn't move it to a different bucket.
- The tracking number is trimmed and upper-cased in the constructor. Normalising at the door means `equals` can use a plain `equals` comparison.
- `getClass() != o.getClass()` rather than `instanceof`. It's stricter: a subclass instance is never equal to a `Parcel`. That keeps symmetry for free. I'd pick it for any class that isn't `final` and has no reason to be compared across a hierarchy. If you need cross-subtype equality, make the class `final` and use `instanceof`, or put the equality rules in one base class and make `equals` final there.
- `Objects.hash(carrier, trackingNumber)` is readable and correct. It allocates an array per call, which only matters in a very hot path. If a profiler ever points at it, write `31 * carrier.hashCode() + trackingNumber.hashCode()` by hand.

Checkpoint: compile everything so far.

```bash
javac -d out $(find src -name '*.java')

```

On Windows PowerShell, list the files explicitly, since `find` isn't there. You should see no output.

### Step 5: Add the one-liner version

Goal: the shortest correct answer when your class is a plain data holder.

`src/main/java/com/example/parcels/ParcelRecord.java`

```java
package com.example.parcels;

/** The short version: a record gets a correct equals/hashCode over all components. */
public record ParcelRecord(String carrier, String trackingNumber) {
    public ParcelRecord {
        trackingNumber = trackingNumber.trim().toUpperCase();
    }
}

```

A record gets `equals`, `hashCode` and `toString` generated over every component. The compact constructor normalises the tracking number, same as `Parcel`. If all of an object's fields define identity, this is what I'd write. The catch is that you can't leave a field out of equality. A parcel with a mutable weight would want to be a class. More on this in [how to use Java records](https://ggorantala.dev/how-to-use-java-records/?ref=ggorantala.dev).

### Step 6: Write the demo

Goal: run all five scenarios in one program.

`src/main/java/com/example/parcels/App.java`

```java
package com.example.parcels;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

public class App {

    public static void main(String[] args) {
        System.out.println("--- 1. equals without hashCode ---");
        Set<BrokenParcel> broken = new HashSet<>();
        broken.add(new BrokenParcel("1Z999"));
        System.out.println("a.equals(b): " + new BrokenParcel("1Z999").equals(new BrokenParcel("1Z999")));
        System.out.println("set contains an equal parcel? " + broken.contains(new BrokenParcel("1Z999")));
        broken.add(new BrokenParcel("1Z999"));
        System.out.println("set size after adding an 'equal' one: " + broken.size());

        System.out.println("--- 2. mutable key ---");
        Map<MutableKeyParcel, String> status = new HashMap<>();
        MutableKeyParcel key = new MutableKeyParcel("1Z999");
        status.put(key, "In transit");
        System.out.println("before change: " + status.get(key));
        key.setTrackingNumber("1Z000");
        System.out.println("after change:  " + status.get(key));
        System.out.println("map still holds it: " + status.size() + " entry, "
                + "containsKey=" + status.containsKey(key));

        System.out.println("--- 3. subclass breaks symmetry ---");
        LooseParcel plain = new LooseParcel("1Z999");
        ExpressParcel express = new ExpressParcel("1Z999", "9-11");
        System.out.println("plain.equals(express): " + plain.equals(express));
        System.out.println("express.equals(plain): " + express.equals(plain));

        System.out.println("--- 4. the fixed Parcel ---");
        Parcel a = new Parcel("UPS", " 1z999 ", "Asha", 1200);
        Parcel b = new Parcel("UPS", "1Z999", "Someone else", 90);
        Set<Parcel> parcels = new HashSet<>();
        parcels.add(a);
        System.out.println("a.equals(b): " + a.equals(b));
        System.out.println("hash codes equal: " + (a.hashCode() == b.hashCode()));
        System.out.println("set contains b: " + parcels.contains(b));
        a.reweigh(1300);
        System.out.println("after reweigh, set still contains a: " + parcels.contains(a));
        System.out.println("different carrier equal? " + a.equals(new Parcel("DHL", "1Z999", "Asha", 1300)));
        System.out.println("equals(null): " + a.equals(null));

        System.out.println("--- 5. record ---");
        System.out.println(new ParcelRecord("UPS", " 1z999 ").equals(new ParcelRecord("UPS", "1Z999")));
    }
}

```

## Run it

```bash
javac -d out $(find src -name '*.java')
java -cp out com.example.parcels.App

```

The output is the block you saw at the top. Section 2 is the quietest bug of the lot. `status.size()` is 1, so the entry is still in the map. It's just unreachable by key lookup, and it won't be garbage collected while the map lives. That's a slow memory leak with no error attached.

## Prove it with checks

I couldn't download JUnit in this environment (Maven Central was blocked from my machine), so the checks are a small `main` method run with assertions on. In your own project, turn each `check` line into a JUnit 5 test; I'll cover that in [how to write unit tests with JUnit 5](https://ggorantala.dev/how-to-write-unit-tests-junit-5-java/?ref=ggorantala.dev).

`src/main/java/com/example/parcels/ParcelChecks.java`

```java
package com.example.parcels;

import java.util.HashSet;
import java.util.Set;

/** Plain checks, run with java -ea. No test framework: nothing to download. */
public class ParcelChecks {

    static int passed = 0;

    static void check(String name, boolean ok) {
        if (!ok) throw new AssertionError("FAILED: " + name);
        System.out.println("ok   " + name);
        passed++;
    }

    public static void main(String[] args) {
        Parcel a = new Parcel("UPS", "1Z999", "Asha", 1200);
        Parcel b = new Parcel("UPS", "1Z999", "Ben", 50);
        Parcel c = new Parcel("UPS", "1Z999", "Chi", 7);
        Parcel other = new Parcel("UPS", "1Z000", "Asha", 1200);

        check("reflexive", a.equals(a));
        check("symmetric", a.equals(b) && b.equals(a));
        check("transitive", a.equals(b) && b.equals(c) && a.equals(c));
        check("consistent across calls", a.equals(b) == a.equals(b));
        check("null is never equal", !a.equals(null));
        check("different tracking number not equal", !a.equals(other));
        check("different carrier not equal", !a.equals(new Parcel("DHL", "1Z999", "Asha", 1200)));
        check("equal objects have equal hash codes", a.hashCode() == b.hashCode());
        check("tracking number normalised", new Parcel("UPS", " 1z999 ", "x", 1).equals(a));
        check("different class not equal", !a.equals("1Z999"));

        Set<Parcel> set = new HashSet<>();
        set.add(a);
        check("HashSet finds an equal parcel", set.contains(b));
        set.add(b);
        check("HashSet keeps one of two equal parcels", set.size() == 1);
        a.reweigh(5000);
        check("hash code survives a reweigh", set.contains(a));

        Set<BrokenParcel> broken = new HashSet<>();
        broken.add(new BrokenParcel("X"));
        check("BrokenParcel without hashCode is lost (this is the bug)",
                !broken.contains(new BrokenParcel("X")));

        LooseParcel plain = new LooseParcel("1");
        ExpressParcel exp = new ExpressParcel("1", "9-11");
        check("LooseParcel/ExpressParcel is asymmetric (this is the bug)",
                plain.equals(exp) && !exp.equals(plain));

        try {
            new Parcel(null, "1", "x", 1);
            check("null carrier rejected", false);
        } catch (NullPointerException e) {
            check("null carrier rejected: " + e.getMessage(), true);
        }
        System.out.println(passed + " checks passed");
    }
}

```

```bash
java -ea -cp out com.example.parcels.ParcelChecks

```

```log
ok   reflexive
ok   symmetric
ok   transitive
ok   consistent across calls
ok   null is never equal
ok   different tracking number not equal
ok   different carrier not equal
ok   equal objects have equal hash codes
ok   tracking number normalised
ok   different class not equal
ok   HashSet finds an equal parcel
ok   HashSet keeps one of two equal parcels
ok   hash code survives a reweigh
ok   BrokenParcel without hashCode is lost (this is the bug)
ok   LooseParcel/ExpressParcel is asymmetric (this is the bug)
ok   null carrier rejected: carrier
16 checks passed

```

The first five checks are the contract from `Object.equals`: reflexive, symmetric, transitive, consistent, and false for null. The `hashCode` check is the other half: equal objects must have equal hash codes. The reverse isn't required. Unequal objects may collide.

## The parts that matter

`HashMap.get(key)` does two things. It calls `key.hashCode()` to choose a bucket, then walks that bucket calling `equals`. So a wrong `hashCode` hides objects, and a wrong `equals` finds the wrong ones. Both methods have to describe the same notion of "the same thing".

The Javadoc contract for `Object.hashCode` boils down to this: the value must stay the same while the fields used by `equals` stay the same, and equal objects must produce equal values. Nothing says the value has to be unique, and nothing says it has to be stable between runs. Don't persist hash codes.

For performance, how well your `hashCode` spreads values matters too: all-equal hash codes turn a map into a list. That's the territory of the HashMap performance article in this site's Performance series; it's planned but not published yet, so I won't link to a page that may not exist.

## Errors I hit, and the fixes

I forced each one on purpose so the messages are exact.

**`method does not override or implement a method from a supertype`**

```log
Over.java:5: error: method does not override or implement a method from a supertype
    @Override
    ^

```

I'd written `public boolean equals(Over o)` instead of `equals(Object o)`. That is an overload, not an override. Always put `@Override` on both methods; this is the only reason it complains.

Without the annotation it compiles and bites you later. I removed `@Override` and ran it:

```log
overload says true
set says false
via Object ref: false

```

Direct calls use your overload; `HashSet` and `List.contains` call `equals(Object)`, which is still `Object`'s identity check.

**Arrays inside `Objects.hash`**

```log
Objects.hash(x)==Objects.hash(y): false
Arrays.hashCode equal: true

```

Two `int[]` with the same content produce different `Objects.hash` values, because arrays use identity hashing. Use `Arrays.hashCode` and `Arrays.equals` for array fields, or `Arrays.deepEquals` for nested ones.

**Floating-point fields**

```log
0.0 == -0.0: true, Double.equals: false
NaN == NaN: false, Double.compare: 0

```

Use `Double.compare(a, b) == 0` in `equals`, and `Double.hashCode` in `hashCode`. Using `==` on doubles disagrees with the boxed rules.

**Lost entries in a HashMap.** No error, just `null` from `get`. Either `hashCode` is missing (Step 1) or a field in the key changed (Step 2). Make key fields `final`.

**IDE generators.** Both IDEs ask which fields to include. Tick only identity fields. IntelliJ also offers a template choice; `java.util.Objects.equals() and hashCode()` is the one I'd use. Lombok's `@EqualsAndHashCode` has a similar choice to make, and I'll look at it when the Lombok setup article is out.

## Before you ship it

- **JPA entities.** Don't use all fields, and be careful with generated ids: an id that's `null` before `persist` changes the hash code afterwards. A business key (like carrier plus tracking number here) is safer. That's its own article.
- **Hibernate proxies.** `getClass()` comparison fails against a proxy subclass. Entities are the one place where the `instanceof` form is usually right.
- **Mutable objects as keys.** Prefer immutable keys. See [how to create an immutable class in Java](https://ggorantala.dev/how-to-create-immutable-class-java/?ref=ggorantala.dev).
- **Builders.** If you build parcels with a builder, equality still lives on the built object. See [how to implement the builder pattern in Java](https://ggorantala.dev/how-to-implement-builder-pattern-java/?ref=ggorantala.dev).
- **Version note.** Java 16 added pattern matching for `instanceof` and records. Before that you'd write the cast by hand. Java 7 added `Objects.equals` and `Objects.hash`; before that, null-safe comparison was your job.

## The short version

- Override `equals` and `hashCode` together, with `@Override` on both.
- Use only fields that define identity, and make them `final` if the object will be a key.
- Pick `getClass()` or `instanceof` on purpose. If you pick `instanceof`, make the class `final`.
- Use `Arrays.hashCode`/`Arrays.equals` for arrays and `Double.compare` for doubles.
- Prove it with checks: contract tests, plus "does a `HashSet` find an equal copy?".

## FAQ: equals() and hashCode() questions I get

**Do I always need to override hashCode when I override equals?**  
Yes. Skip it and anything in a hash-based collection (`HashMap`, `HashSet`, `Collectors.toMap`) can misbehave, as Section 1 showed. The reverse isn't strictly required, but a `hashCode` without `equals` is pointless.

**Should I use getClass() or instanceof in equals?**  
`getClass()` keeps symmetry when subclasses add state. `instanceof` is fine on a `final` class, or when a base class defines equality for the whole hierarchy and marks `equals` final. Hibernate entities are the usual exception.

**Can I use a record instead of writing equals and hashCode?**  
If every component defines identity, yes. A record compares all components and you can't exclude one. For anything with changing state, a class is the better fit.

**Why does Objects.hash give different results for equal arrays?**  
Arrays inherit identity-based `hashCode` from `Object`. Use `Arrays.hashCode` for arrays.

## References

- [Object.equals and Object.hashCode, Java SE 21 API](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Object.html?ref=ggorantala.dev)
- [java.util.Objects, Java SE 21 API](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Objects.html?ref=ggorantala.dev)
- [JEP 395: Records](https://openjdk.org/jeps/395?ref=ggorantala.dev) and [JEP 394: Pattern Matching for instanceof](https://openjdk.org/jeps/394?ref=ggorantala.dev)