Java hashCode() and equals() Contract

The topic of the equals() and hashCode() contract in Java is so important that its proper understanding is essential to implement correctly and efficiently working solutions. Poor implementation of the hashCode() and equals() methods can cause a lot of trouble for the applications you develop, which is why it is so important to explore this topic more extensively.

In this article, you will learn how to avoid bugs and performance problems when implementing hashCode() and equals() in your application.

How does equals() work?

Since every Java class inherits from the Object class, and Object implements the equals() method – every Java class by default implements equals(). The implicit implementation compares objects by their reference, and this is often not the right solution.

public boolean equals(Object obj) {
  return (this == obj);
}

In practice, the above method will only return true for objects stored in the same memory location. Whereas when calling the equals() method our intention should be to compare the values of the objects. Let’s consider following example:

class Car {

  private final String concern;
  private final String model;
  private final int productionYear;

  public Car(String concern, String model, int productionYear) {
    this.concern = concern;
    this.model = model;
    this.productionYear = productionYear;
  }
}

@Test
void shouldBeEqualIfAllValuesAreTheSame() {
  Car car1 = new Car("Toyota", "Prius", 2022);
  Car car2 = new Car("Toyota", "Prius", 2022);

  assertEquals(car1, car2);
}

The above test will fail, even though each value of both Car instances is the same. This is because each object in Java created with a new keyword is stored in a different memory location. Therefore, in order to correctly compare the values of objects, the equals() method should be overridden.

For this purpose, you can use the IDE or code-generating libraries, keeping in mind a few important things that we have mentioned in his article: Pitfalls of automatically generated code.

@Override
public boolean equals(Object o) {
  if (this == o) {
    return true;
  }
  if (o == null || getClass() != o.getClass()) {
    return false;
  }

  Car car = (Car) o;

  if (productionYear != car.productionYear) {
    return false;
  }
  if (!Objects.equals(concern, car.concern)) {
    return false;
  }
  return Objects.equals(model, car.model);
}

Overriding equals() method, as indicated above, will cause the shouldBeEqualIfAllValuesAreTheSame() test to pass correctly. To implement correct equals() method, following things need to be fulfilled:

  • It has to be reflexive, so for any non-null value, x.equals(x) should return true.
  • It has to be symmetric, so for any non-null value when x.equals(y) returns true, then y.equals(x) has to return true as well.
  • It has to be transitive, so for any non-null value if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) has to return true.
  • It has to be consistent, so multiple invocations of x.equals(y) have to return the same value for each call.
  • It has to return false for any non-null value calling x.equals(null).

How does hashCode() work?

The most important thing to remember about hashCode() is that each time we override equals() implementation we have to override hashCode() as well. This is essential for the correct operation with the hash-based collections. Let’s assume that we have our current implementation of Car.java, but we didn’t override hashCode() yet. What will happen when we try to work with HashMap?

@Test
  void shouldContainOnlyOneKeyWhenAddingEqualCars() {
    Car car1 = new Car("Toyota", "Prius", 2022);
    Car car2 = new Car("Toyota", "Prius", 2022);

    Map<Car, String> cars = new HashMap<>();
    cars.put(car1, "This is car1");
    cars.put(car2, "This is car2");

    assertEquals(cars.keySet(), Set.of(car2));
  }

The values in the hashMap should be unique. When adding a new entry to a HashMap, firstly hashCode() is computed and based on its value the entry is added to the proper bucket. Then the new object is compared with other objects inside the bucket with the equals() method.

If equals() returns true, then the new object replaces the old one and if equals() returns false, then the new object is simply added to the bucket alongside the other objects. That is why the correct implementation of the hashCode() method is so important.

The above test will fail, because cars keySet will contain both keys: car1 and car2 as hashCode() will return different results, even though each value inside the objects are the same, while only car2 key should be left as it should replace car1. In order to pass the test correctly, the hashCode() method has to be overridden.

@Override
  public int hashCode() {
    int result = concern != null ? concern.hashCode() : 0;
    result = 31 * result + (model != null ? model.hashCode() : 0);
    result = 31 * result + productionYear;
    return result;
  }

Another important thing to remember is that when any value inside the object is changed, and this value is used inside the hashCode() method, then the result of hashCode() will also change. This is crucial when using an object as a key in HashMap.

Let’s assume that int productionYear is not final anymore, the setter method setProductionYear() is added to Car.java and we use Car instances as keys. In this case, changing the value will make it impossible to extract the value from the HashMap as shown in the test below.

@Test
void shouldReturnNullIfValueChanged() {
  Car car = new Car("Toyota", "Prius", 2022);

  Map<Car, String> cars = new HashMap<>();
  cars.put(car, "This is Toyota Prius 2022");

  assertEquals(cars.get(car), "This is Toyota Prius 2022");
    
  car.setProductionYear(2000);
    
  assertNull(cars.get(car));
}

Following statements have to be fulfilled to provide correct implementation of the hashCode() method:

  • It has to return the same result whenever it is invoked on the same object more than once during the runtime of application.
  • If equals() returns true, then hashCodes of both objects have to be equal.
  • If equals() returns false, then it is not required for hashCodes to be different for both objects, but it is reasonably practical and recommended.
  • If hashCode() is different for objects, then equals() has to return false.

Summarize

So what is the hashCode() equals() contract actually? Basically it is a set of rules that are already outlined above, and meeting them ensures the proper functioning of the application. HashCode is a kind of shortcut for an object that should be as unique as possible for optimization purposes, whereas equals() should check the equality of objects as precisely as possible.