Skip to content

equals() and hashCode()

equals() decides when two objects are the same value. hashCode() helps HashMap and HashSet find them. Override both.

Stage 1core5 min read

What are they?

Every Java object inherits two methods from Object:

  • equals(Object other) answers “do these two objects count as the same value?”
  • hashCode() returns an int (a number). Hash-based collections such as HashMap and HashSet use it to decide where to look for an object.

equals defines what “the same” means for your class. hashCode helps maps and sets find the object fast. Equal objects must always return the same hash code.

The inherited Object.equals checks identity: is it the very same object in memory? So two objects built separately are unequal, even when all their fields match, unless you override equals.

Vehicle a = new Car("KA01AB1234");
Vehicle b = new Car("KA01AB1234");

System.out.println(a == b);        // false, two different objects
System.out.println(a.equals(b));   // false too, because nobody overrode equals

That second false is usually not what you want. The same car has the same plate, so they should be equal.

Why this matters more than it looks

HashMap and HashSet do not compare objects field by field. They do this (a bucket is one slot in the map's storage):

1. Call hashCode() on the key
2. Use it to pick a bucket
3. Inside that bucket, use equals() to find the exact key

So if you put a custom object in a map or set and did not override both methods, lookups silently fail.

Map<Vehicle, ParkingSpot> spots = new HashMap<>();
spots.put(new Car("KA01AB1234"), spotA);

// Later, a different Car object with the same plate
ParkingSpot found = spots.get(new Car("KA01AB1234"));

System.out.println(found);   // null

Nothing throws. Nothing logs. You just get null and the car appears to have never parked. This is the classic Java bug.

Where this matters in this repo

Several classes are used as HashMap keys or in a HashSet. These key classes override equals and hashCode in the repo:

WhereUsed asKey class that overrides both
OOOD/01-parking-lot/ParkingManager.javaMap<Vehicle, ParkingSpot>, Map<ParkingSpot, Vehicle>Car, Truck, Motorcycle, and the spot classes
OOOD/11-restaurant-management-system/Table.javaMap<MenuItem, List<OrderItem>>MenuItem (by name)
OOOD/11-restaurant-management-system/ReservationManager.javaSet<Reservation>Reservation
OOOD/02-movie-ticket-booking/ScreeningManager.javaMap<Movie, List<Screening>>, Map<Screening, List<Ticket>>Movie, Screening

The one remaining case is OOOD/09-shipping-locker-system/Site.java, which holds Set<Locker>. Locker does not override either method, so the set compares by identity. That works today because the code always passes back the same Locker object. If you build a new object from user input, a database row, or JSON, an identity-based lookup returns null.

Note that Map<VehicleSize, ...> and Map<LockerSize, ...> are fine, because enums get correct equals and hashCode from Java. See 14-Enums.md.

The contract

If you override one, you must override the other. These rules are what collections rely on:

  1. Equal objects must have equal hash codes. If a.equals(b) then a.hashCode() == b.hashCode(). Breaking this is what causes the null lookup above.
  2. Unequal objects may share a hash code. That is called a collision. It is fine: the map then uses equals to tell them apart inside the bucket.
  3. Reflexive: a.equals(a) is true. An object equals itself.
  4. Symmetric: if a.equals(b) then b.equals(a). The order does not matter.
  5. Transitive: if a.equals(b) and b.equals(c) then a.equals(c). Equality carries along a chain.
  6. Consistent: the answer does not change unless the fields change.
  7. a.equals(null) is always false. It must not throw.
The one rule to remember:

    equal objects  ->  same hashCode          (required)
    same hashCode  ->  equal objects          (NOT required)

How to write them

public class Car {
    private final String licensePlate;

    public Car(String licensePlate) {
        this.licensePlate = licensePlate;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;                    // same reference, quick exit
        if (o == null || getClass() != o.getClass()) return false;
        Car other = (Car) o;
        return Objects.equals(licensePlate, other.licensePlate);   // compare the identity fields
    }

    @Override
    public int hashCode() {
        return Objects.hash(licensePlate);             // same fields as equals
    }
}

Line by line:

  • this == o is a quick exit: both names point to the same object, so they are equal.
  • getClass() != o.getClass() rejects objects of another class. It also rejects subclasses, so equals stays symmetric.
  • Objects.equals(a, b) handles nulls for you, so you do not need a null check on each field.
  • Objects.hash(...) builds a hash from any number of fields.

The fields in hashCode must be the same fields you used in equals. Here only licensePlate is used, because two cars with the same plate are the same car.

Picking the fields

Only use the fields that define identity. Not every field.

ClassGood identity fieldsDo not include
Carlicence platecurrent spot, colour, owner name
Reservationreservation id, or table + timeparty size, notes
Moviemovie id, or title + release yearcurrent showtimes, poster URL
MenuItemname, or item idprice (it changes), description

If the class has a real id, use just the id. It is simpler and it survives field changes.

Mutable keys: the trap

Never build hashCode from a field that can change while the object sits in a map or set.

Set<Reservation> set = new HashSet<>();
Reservation r = new Reservation(table, LocalDateTime.of(2026, 8, 16, 19, 0));
set.add(r);

r.setTime(LocalDateTime.of(2026, 8, 16, 20, 0));   // hashCode just changed

System.out.println(set.contains(r));   // false, even though it is literally the same object

The hash changed, so the set now looks in a different bucket from the one where the object was stored. The object cannot be found, and it cannot even be removed. Fix it by making the identity fields final, or by using an id that never changes. The repo's Reservation does this, with all fields final.

Never compare with ==

For objects, == asks “is it the same object?”, not “is the content the same?”

String a = new String("hello");
String b = new String("hello");
a == b        // false
a.equals(b)   // true

== is only correct for primitives (int, boolean, char) and enums.

Checking it works

Car c1 = new Car("KA01AB1234");
Car c2 = new Car("KA01AB1234");

c1.equals(c2)                    // true
c1.hashCode() == c2.hashCode()   // true, required

Map<Car, ParkingSpot> spots = new HashMap<>();
spots.put(c1, spotA);
spots.get(c2)                    // spotA, works now

If you write only one test, write the last line (spots.get(c2)).

Shortcuts

Your IDE generates both from selected fields. In IntelliJ press alt + insert and choose "equals() and hashCode()". Always check which fields it picked.

If the class is a plain value holder, a record (a short way to declare a class that only holds values) gives you correct equals, hashCode and toString automatically:

public record Money(BigDecimal amount, String currency) { }

Use records for value objects. Write the methods by hand when only one field out of many defines identity.

Common mistakes

  • Overriding only one of the two. The most common cause of the null lookup bug.
  • Writing equals(Car other) instead of equals(Object o). That is an overload (a different method with the same name), not an override, so the collection never calls it. Always add @Override so the compiler catches this.
  • Using mutable fields in hashCode. See the trap above.
  • Returning a constant from hashCode. It is technically legal, but every entry lands in one bucket, so the map has to scan them one by one, like a linked list.
  • Including every field. More fields means more ways for two objects that should be the same to come out different.

Where you see it in this repo

The key classes in the table above override both methods. See Car in OOOD/01-parking-lot, which matches the example in this note. Locker in the shipping locker system is the one used in a set without them.

Related: 14-Enums.md for why enum keys are already safe, and 17-Collections.md for which collections depend on these methods.

In this topic

Done with this topic?

How well do you know it?

PreviousEnums