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 anint(a number). Hash-based collections such asHashMapandHashSetuse 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 equalsThat 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); // nullNothing 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:
| Where | Used as | Key class that overrides both |
|---|---|---|
OOOD/01-parking-lot/ParkingManager.java | Map<Vehicle, ParkingSpot>, Map<ParkingSpot, Vehicle> | Car, Truck, Motorcycle, and the spot classes |
OOOD/11-restaurant-management-system/Table.java | Map<MenuItem, List<OrderItem>> | MenuItem (by name) |
OOOD/11-restaurant-management-system/ReservationManager.java | Set<Reservation> | Reservation |
OOOD/02-movie-ticket-booking/ScreeningManager.java | Map<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:
- Equal objects must have equal hash codes. If
a.equals(b)thena.hashCode() == b.hashCode(). Breaking this is what causes the null lookup above. - Unequal objects may share a hash code. That is called a collision. It is fine: the map then uses
equalsto tell them apart inside the bucket. - Reflexive:
a.equals(a)is true. An object equals itself. - Symmetric: if
a.equals(b)thenb.equals(a). The order does not matter. - Transitive: if
a.equals(b)andb.equals(c)thena.equals(c). Equality carries along a chain. - Consistent: the answer does not change unless the fields change.
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 == ois 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, soequalsstays 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.
| Class | Good identity fields | Do not include |
|---|---|---|
Car | licence plate | current spot, colour, owner name |
Reservation | reservation id, or table + time | party size, notes |
Movie | movie id, or title + release year | current showtimes, poster URL |
MenuItem | name, or item id | price (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 objectThe 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 nowIf 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 ofequals(Object o). That is an overload (a different method with the same name), not an override, so the collection never calls it. Always add@Overrideso 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.