Friends of OpenJDK Today

Equals and Hashcode Implementation Considerations

November 21, 2021

Author(s)

  • Wim Deblauwe

    Wim Deblauwe is a freelance Java Developer, blogger, open-source enthusiast and author of “Taming Thymeleaf”, a book on working with Spring Boot and Thymeleaf.

I always struggled with how to implement equals and hashcode, until I learned about the difference between entities and value objects.

Why implement equals and hashcode?

All classes in Java inherit from java.lang.Object.

The equals() and hashCode() methods are two important methods that you usually should override when defining your own classes.

equals() is important for comparing 2 objects to check if they represent the same thing.

We will see in a bit what that means exactly for different types of objects.

hashCode() is important if you put your object in a HashSet or a HashMap. It facilitates the hashing that is used by those data structures.

Entity vs Value Object

Even if you don’t know Domain Driven Design, you might have heared about entities and value objects.
If you have not, here is a small recap about their differences:

  • Entity: An object that has a distinct identity within the application domain. For instance, a User or an Invoice.
  • Value Object: Objects that only matter because of the value they represent. For instance, a Money or Temperature object. Usually, these objects are immutable.

Equals and hashcode for value objects

Let’s imagine a fairly simple value object that represents temperature.

It has a value and a unit and the code could look something like this:

public class Temperature {
    private final double value;
    private final Unit unit;

    public Temperature(double value,
                       Unit unit) {
        this.value = value;
        this.unit = unit;
    }

    public double getValue() {
        return value;
    }

    public Unit getUnit() {
        return unit;
    }

    enum Unit {
        KELVIN, CELCIUS, FAHRENHEIT;
    }
}

For value objects, we want to state that objects are equal when all of their properties are equal.

The implementation should be this:

public class Temperature {

    ...

    @Override
    public boolean equals(Object o) {
        if (this == o) { //
            return true;
        }
        if (o == null || getClass() != o.getClass()) { //
            return false;
        }
        Temperature that = (Temperature) o; //
        return Double.compare(that.value, value) == 0 && unit == that.unit; //
    }

    @Override
    public int hashCode() {
        return Objects.hash(value, unit); //
    }

    ...
}

Short-circuit if the passed in object is the same reference (in memory) as the current object. An object can never be equal to `null` and it cannot be equal to an object of another class. We can safely cast the passed in object as we are sure it is of the same class as this object. Compare each of the properties of the passed in object with the current object Use the JDK `Objects.hash()` method to generate a hash code using all of the properties of the current object.

We can validate now that 2 Temperature objects with the same properties are equal:

@Test
void testEqualTemperature() {
    Temperature temperature1 = new Temperature(37.0, Temperature.Unit.CELCIUS);
    Temperature temperature2 = new Temperature(37.0, Temperature.Unit.CELCIUS);

    boolean equal = temperature1.equals(temperature2);
    assertTrue(equal);
}

I explictly called the equals() method here in the test, but this is not how you would normally do this.
Either you would use the assertEquals() method of JUnit, or the assertThat(..).isEqualTo(..) method of AssertJ, both of which will call equals() internally in the end.

We can test our hashCode() implementation like this:

@Test
void testHashCodeForEqualObjects() {
    Temperature temperature1 = new Temperature(37.0, Temperature.Unit.CELCIUS);
    Temperature temperature2 = new Temperature(37.0, Temperature.Unit.CELCIUS);

    int hashCode1 = temperature1.hashCode();
    int hashCode2 = temperature2.hashCode();

    assertThat(hashCode1).isEqualTo(hashCode2);
}

We test that equal objects should give equal hash codes.

Note that the opposite does not need to be true.

Different objects (as determined by the equals() implementation) can return the same hashcode, this is not a problem at all.

Equals and hashcode for entities

For an entity, all that really matters is the identifier.

We want to see 2 instances that have the same identifier as the same thing, even if other properties are different.

Suppose this simple User entity:

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class User {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    protected User() {
    }

    public User(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Since we only care about the id field, a naive implementation would look like this:

// Don't do this for your entities!

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

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

Unfortunately, this is wrong.

The problem is that the id field is generated by the database and only filled in after the object is persisted.

So for the same object, the id is initially null and then gets a certain value after it is stored in the database.

Luckily, Vlad Mihalcea shows us how to implement this correctly:

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

    @Override
    public int hashCode() {
        return getClass().hashCode();
    }

2 important notes:

  • We will only see instances of User as equal if the id is filled in. 2 User instances that both have not been stored in the database will never be equal.
  • Hashode uses a hardcoded value, because it is not allowed that a hashCode value changes between the time the object is created and the time it is persisted in the database.

See How to implement equals and hashCode using the JPA entity identifier (Primary Key) for more in-depth details on this.

Equals and hashcode for entities using early primary key generation

If you don’t like the way we need to implement equals() and hashCode() for JPA entities, then there is a different route you can take.

When you generate the primary key before you create the object, there are 2 advantages:

  1. The id can be made required in the constructor so you can’t create "invalid" objects.
  2. The equals() and hashCode() methods can be simplified to just take the id into account.

In code, we can imagine this entity:

import org.springframework.util.Assert;

import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
public class Book {
    @Id
    private Long id;

    private String name;

    protected Book() {
    }

    public Book(Long id,
                String name) {
        Assert.notNull(id, "id should not be null");
        Assert.notNull(name, "name should ot be null");
        this.id = id;
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

The Book entity does not have the @GeneratedValue annotation, so we will need to pass in a value at construction time.

Now that we know the id field is never null, we can use this implementation:

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

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

We just use id for equals(), and we can relay on id as well for hashCode()

Note If you like to use early primary key generation, then check out my open-source library JPearl. It has base classes and a Maven plugin that makes the implementation of this a breeze.

A test on equals could look like this:

    @Test
    void testEquals() {
        Book book1 = new Book(1L, "Taming Thymeleaf");
        Book book2 = new Book(1L, "Taming Thymeleaf");

        assertThat(book1).isEqualTo(book2);
    }

Since we only test the id, this test will also succeed:

    @Test
    void testEquals() {
        Book book1 = new Book(1L, "Taming Thymeleaf");
        Book book2 = new Book(1L, "Totally different title");

        assertThat(book1).isEqualTo(book2);
    }

This might be counter-intuative at first, but this is really what you want.

Entities are defined by their id, when the id is the same, we are talking about the same thing.

Testing equals and hashCode implementations

The tests that I have shown here only scratch the surface of all the things that you need to test to fully implement the equals() and hashCode contracts.

To ensure your methods are correctly implemented, use EqualsVerifier.

Add it to your pom.xml:

    nl.jqno.equalsverifier
    equalsverifier
    3.6
    test

And write the test:

    @Test
    public void equalsContract() {
        EqualsVerifier.forClass(Temperature.class).verify();
    }

This will test if equals() is reflexive, symmetric, transitive and consistent. It also tests if hashCode() adheres to the contract defined in the java.lang.Object API.

Note When writing the blog entry, the test pointed out equals of Temperature was not final (See https://jqno.nl/equalsverifier/errormessages/subclass-equals-is-not-final/). The best fix was to make the whole class final as the class was not intended to be subclassed anyway. So verifying your implementation is certainly worth it.

Conclusion

To correctly implement the equals() and hashCode(), it is important to first determine if your object is a value object or an entity.

If it is one of the those, you can follow the rules set forth in this article. If it is neither (e.g., a Controller, Service, Repository​) then you probably don’t want to override the methods.

Related Articles

View All
  • Better Error Handling for Your Spring Boot REST APIs

    One of the things that distinguishes a decent API from one that is a pleasure to work with is robust error handling. Nothing is more frustrating than using some API and getting back cryptic errors where you can only guess why the server is not accepting your request.

    Spring Boot lets you customize the error handling for your application, but there is quite a lot of low-level coding involved if you want to do this correctly.

    Read More
    August 21, 2021
  • For the Record!

    Ever since Java announced their 6-month release cycle, there is excitement around exploring new features and even more so with preview features.

    Now, what is a record? It is a new variety of type declaration. It is also a sub-type of class. A common type of class, as we all know, is the data-carrier class. They are classes that have some fields and their corresponding getters and setters. They usually have little to no logic.

    Records help provide a way to succinctly describe the intent of these data-carrier classes. A little less conversation, a little more action.

    Read More
    Avatar photo
    November 13, 2020
  • Generating Code with IntelliJ IDEA

    One of the super cool things about IntelliJ IDEA is how much code you can generate with minimum effort.

    There’s a Generate menu in IntelliJ IDEA that you can access with ⌘N on macOS and Alt+Insert on Windows and Linux.

    Here’s a quick tour of some of the places where you can use it in Java projects in IntelliJ IDEA.

    Read More
    February 10, 2021

Author(s)

  • Wim Deblauwe

    Wim Deblauwe is a freelance Java Developer, blogger, open-source enthusiast and author of “Taming Thymeleaf”, a book on working with Spring Boot and Thymeleaf.

Comments (0)

Your email address will not be published. Required fields are marked *

Highlight your code snippets using [code lang="language name"] shortcode. Just insert your code between opening and closing tag: [code lang="java"] code [/code]. Or specify another language.

Save my name, email, and website in this browser for the next time I comment.

Subscribe to foojay updates:

https://foojay.io/feed/
Copied to the clipboard