Simplifying equality and debugability

(Daniel Pfeifer)
The methods equals, hashCode and toString. Many classes need them, many developers hate them. They are fundamental methods required to build robust classes and while you may or may not rely on your IDE to generate them for you, you still feel that they are not quite the way they should be.
When using an IDE, PMD, Findbugs and CheckStyle will most certainly complain about this methods. What's worse, sometimes they aren't even fit for purpose (especially when using Hibernate) as you are comparing class members rather than comparing using the proxy's accessor-methods.
So let's try once more to make our lives just a little bit easier by combining hand-crafted equals/hashCode and toString-methods that CheckStyle won't complain about and that aren't all too difficult to understand.
If you're using Maven, now is a good time to add following dependency to your POM:

<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>17.0</version>
</dependency>

Yes, this tutorial will use Google's Guava, but truth be told: commons-lang and commons-lang3 will work fine as well (you just have to use different helper methods).
Let's contemplate this monstrosity:

@Entity
public class JpaEntity {
    public Long id;
    public String firstName;
    public String lastName;
    public String mailAddress;
    public Date dateOfBirth;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getMailAddress() {
        return mailAddress;
    }

    public void setMailAddress(String mailAddress) {
        this.mailAddress = mailAddress;
    }

    public Date getDateOfBirth() {
        return dateOfBirth;
    }

    public void setDateOfBirth(Date dateOfBirth) {
        this.dateOfBirth = dateOfBirth;
    }

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

        JpaEntity jpaEntity = (JpaEntity) o;

        if (dateOfBirth != null ? !dateOfBirth.equals(jpaEntity.dateOfBirth) : jpaEntity.dateOfBirth != null) return false;
        if (!firstName.equals(jpaEntity.firstName)) return false;
        if (id != null ? !id.equals(jpaEntity.id) : jpaEntity.id != null) return false;
        if (!lastName.equals(jpaEntity.lastName)) return false;
        if (!mailAddress.equals(jpaEntity.mailAddress)) return false;

        return true;
    }

    @Override
    public int hashCode() {
        int result = id != null ? id.hashCode() : 0;
        result = 31 * result + firstName.hashCode();
        result = 31 * result + lastName.hashCode();
        result = 31 * result + mailAddress.hashCode();
        result = 31 * result + (dateOfBirth != null ? dateOfBirth.hashCode() : 0);
        return result;
    }

    @Override
    public String toString() {
        return "JpaEntity{" +
                "id=" + id +
                ", firstName='" + firstName + '\'' +
                ", lastName='" + lastName + '\'' +
                ", mailAddress='" + mailAddress + '\'' +
                ", dateOfBirth=" + dateOfBirth +
                '}';
    }
}

Pretty standard, pretty unreadable, pretty offensive to code quality tools. Worst of all, if you use Hibernate (or any other framework that really returns a proxy rather than the actual class), the equals and hashCode methods may not even be working as desired!
So let's first get used to the fact that we use get-methods (if you can configure your IDE to do so, great!). Rather than "if (dateOfBirth != null ..." we use "if (getDateOfBirth() != null ...".
That in itself is not quite enough, we also want to get rid of all the "if ... return"-statements. Too many returns makes it hard to find the actual exit-point of a method and code quality tools will complain.
Also, instead of constantly checking everything for a null value and calculate the hashCode line by line, we will use Java 7's Objects-util.
Last but not least, let's use Guava's toStringBuilder instead of concatenating a long string with a lot of formatting.
Check this out instead:

@Override
public boolean equals(Object o) {
    boolean retVal = o == this;

    // Allow subclass-equality-check by using this if-statement instead:
    // if (!retVal && o instanceof JpaEntity) {
    if (!retVal && o != null && getClass() == o.getClass()) {
        final JpaEntity that = (JpaEntity) o;

        retVal = Objects.equals(getId(), that.getId()) &&
                Objects.equals(getFirstName(), that.getFirstName()) &&
                Objects.equals(getLastName(), that.getLastName()) &&
                Objects.equals(getMailAddress(), that.getMailAddress()) &&
                Objects.equals(getDateOfBirth(), that.getDateOfBirth());
    }

    return retVal;
}

@Override
public int hashCode() {
    return Objects.hash(getId(), getFirstName(), getLastName(), getMailAddress(), getDateOfBirth());
}

@Override
public String toString() {
    return com.google.common.base.Objects.toStringHelper(this)
            .add("id", getId())
            .add("firstName", getFirstName())
            .add("lastName", getLastName())
            .add("mailAddress", getMailAddress())
            .add("dateOfBirth", getDateOfBirth())
            .toString();
}

A lot more readable, isn't it? Well, with all this said, happy coding!

This post was first released on my `other blog <http://www.mindbug.org/2014/05/simplifying-equality-and-debugability.html>`__.