Question

If I have a simple Groovy class such as

class Address {

  Integer streetNumber
  String streetName
  String state
  String zip
  Country country    
}

Although I could write (or use an IDE to generate) hashCode and equals methods like:

boolean equals(o) {
    if (this.is(o)) return true;

    if (!o || getClass() != o.class) return false;

    Address that = (Address) o;

    if (streetNumber? !streetNumber.equals(that.streetNumber) : that.streetNumber!= null) return false;
    if (streetName? !streetName.equals(that.streetName) : that.streetName!= null) return false;
    if (state? !state.equals(that.state) : that.state!= null) return false;
    if (zip? !zip.equals(that.zip) : that.zip!= null) return false;
    if (country? !zip.equals(that.zip) : that.zip!= null) return false;

    return true;
}

int hashCode() {
    int result = (streetNumber ? streetNumber.hashCode() : 0);
    result = 31 * result + (streetName ? streetName.hashCode() : 0);
    result = 31 * result + (state ? state.hashCode() : 0);
    result = 31 * result + (zip ? zip.hashCode() : 0);
    return 31 * result + (country ? country.hashCode() : 0);
}

Although this will work fine, I feel I could be making better use of Groovy's dynamism to achieve the same thing in a lot less code. One approach that springs to mind is using .properties to get a map of an object's property names and values. I can then iterate over these properties, calling hashCode() or equals() on each one to achieve the same result as above.

Before I go down this path, I just want to check whether anyone else has found a good solution to this problem. I'm a bit wary of rolling my own solution, because the consequences of messing up equals() or hashCode() are potentially dire and hard to track down.

Thanks, Don

Was it helpful?

Solution

I'm not a groovy developer, but I understood that from groovy 1.8 you can invoke the AST transformation using @EqualsAndHashCode on the type.

OTHER TIPS

Or you can just use Apache Commons Lang's EqualsBuilder and HashCodeBuilder. You can either let the builders use Reflection such that it will evaluate all fields or identify which field should be included in the equals() and hashCode() computations.

They also have a ToStringBuilder if you're interested.

If you want a purely Groovy solution, you could do something like this:

interface DefaultEquality {}

DefaultEquality.metaClass.hashCode = {
    delegate.properties.inject(1) { hash, property ->
        if (property.key == "class" || property.key == "metaClass") {
            hash
        } else {
            31 * hash + (property.value?.hashCode() ?: 0)
        }
    }
}

DefaultEquality.metaClass.equals = { obj ->
    def outerDelegate = delegate
    outerDelegate.properties.inject(true) { equals, property ->
        if (property.key == "metaClass") {
            equals
        } else {
            equals && outerDelegate[property.key] == obj[property.key]
        }
    }
}


class Foo implements DefaultEquality {
    String name
    Integer number
}

def a1 = new Foo()
def b1 = new Foo(name: "Delphyne")
def c1 = new Foo(number: 1)
def d1 = new Foo(name: "Delphyne", number: 1)

def a2 = new Foo()
def b2 = new Foo(name: "Delphyne")
def c2 = new Foo(number: 1)
def d2 = new Foo(name: "Delphyne", number: 1)

assert a1 == a2 && a1.hashCode() == a2.hashCode()
assert b1 == b2 && b1.hashCode() == b2.hashCode()
assert c1 == c2 && c1.hashCode() == c2.hashCode()
assert d1 == d2 && d1.hashCode() == d2.hashCode()

You could also implement an AST transformation that does the same thing. You probably should also check that the classes match, as in a traditional equals() method, but that seems to violate the duck-typing principal to me. Adjust as your tastes suit you.

Note that if you're in a script, the classes end up being an anonymous inner class inside the script, so the equality would fail anyway. Normal compiled classes won't suffer this same problem.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top