Effective Java with Groovy - Are you implementing equals and hashCode correctly?

Consider the following Groovy code.

class Product {
    String sku
    String description
    BigDecimal price
}

Now let's put the Product class into use as follows.

Product book1 = new Product(sku: 'P101', description: 'Effective Java', price: 599.0)
Product book2 = new Product(sku: 'P101', description: 'Effective Java', price: 599.0)
println book1 == book2

We create two instances of Products using the same set of values. The above piece of code prints false. Since we didn't override equals, the method defined in java.lang.Object will be inherited by Product class. According to the implementation in java.lang.Object, two instances are equal only when both the references are pointing to the same object.

// Continues from the previous example
def stock = [:]
stock[book1] = 100
println stock[book2]

Here, I create a Map object ([:] creates an empty map) and store a value 100 with object book1 as the key. Then I used the second book object to lookup. Here we end up receiving null. The value returned from the hashCode() method is used in map operations. As per the implementation from java.lang.Object, every instance will have a unique hash code.

Now we understand that we should override equals() and hashCode() methods in Product class. While overriding them, we also need to ensure that whenever two instances are equal, they should also have the same hash code.

A sample implementation is as follows.

class Product {
    String sku
    String description
    BigDecimal price

    boolean equals(o) {
        if (this.is(o)) return true
        if (getClass() != o.class) return false

        Product product = (Product) o

        if (description != product.description) return false
        if (price != product.price) return false
        if (sku != product.sku) return false

        return true
    }

    int hashCode() {
        int result
        result = (sku != null ? sku.hashCode() : 0)
        result = 31 * result + (description != null ? description.hashCode() : 0)
        result = 31 * result + (price != null ? price.hashCode() : 0)
        return result
    }
}

Here I have used the template provided by my IDE, IntelliJ IDEA. One could also make use of utility classes provided by libraries such as "Apache Commons Lang" (It gives EqualsBuilder and HashCodeBuilder). I find two problems with the above approach.

The first one - to maintain the consistency between equals and hashCode, both of these methods have to rely upon the same set of attributes. Hence, we are violating the DRY(Don't Repeat Yourself) principle in the above code. Note how equals and hashCode are on their own in deciding what attributes they should consider.

Second - Often, our code keeps evolving, and we might be adding more fields to our Product class. While IDE code generation works for the first time, it doesn't help when we add another attribute. Developers might also forget to update these methods when they introduce new attributes.

If you are new to Groovy, you might want to get a taste of Groovy from this post.

Now let's explore how Groovy solve's the above two problems. Groovy provides an AST transformation groovy.transform.EqualsAndHashCode, which can be applied to the class as follows.

import groovy.transform.EqualsAndHashCode

@EqualsAndHashCode
class Product {
    String sku
    String description
    BigDecimal price
}

While compiling, Groovy will generate the implementations for both equals and hashCode using all the three defined attributes. You can customise if only specific fields need to be considered for implementing equals and hashCode. An example is as follows.

@EqualsAndHashCode(includes = "sku,description,price")

The attributes to be considered are defined in a single place. Hence we are adhering to the DRY principle. Also, if you add additional attributes, you will have to compile your code, and the equals and hashCode methods get regenerated during compile time . Thus both the above problems are addressed by Groovy.

If you are interested in learning more about how Effective Java applies to Groovy code, you may want to take a look at my GR8Conf EU presentation.

References: