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.
To learn more about Effective Java with Groovy, refer to the collection of posts here.
References: