hilarious…

December 28, 2006 on 9:43 pm | In Funny | No Comments

You wish you had these skills…



equals and hashCode via syntactic-sugar (AKA JDK-1.5 features)

December 2, 2006 on 11:06 pm | In Technical stuff | 10 Comments

Some classes we make may require a proper hashCode and equals method written. Especially if they will be placed in a Collection. Before, when a person who was too “lazy” (or as I like to call it, being a genius) to manually write their own hashCode and equals for each class, they’d possibly look to a library like Commons Lang (CL). CL not only does a great job of modularizing the equals and hashCode generation, but it also follows the guidelines laid in that great book Effective Java. Ok, this is good. However, that’s one more library you need to add to your code, and in my opinion, for a fairly small benefit. I’m not a major proponent of recreating the wheel. However, if it’s just a matter of a few lines of code, I say go for it.

Take for instance, a Person class. A Person will have a name, weight, heightInInches, dateOfBirth and bloodType (an enum). First, let’s look at an example of the equals and hashCode for the Class.


public class Person {
    String name;
    int weight;
    float heightInInches;
    Date dateOfBirth;
    BloodType bloodType; // An Enum of 8 items: O(+/-), A(+/-), B(+/-) and AB(+/-)

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

        final Person person = (Person) o;

        if (Float.compare(person.heightInInches, heightInInches) != 0) return false;
        if (weight != person.weight) return false;
        if (bloodType != person.bloodType) return false;
        if (dateOfBirth != null ? !dateOfBirth.equals(person.dateOfBirth) : person.dateOfBirth != null) return false;
        if (name != null ? !name.equals(person.name) : person.name != null) return false;

        return true;
    }

    public int hashCode() {
        int result;
        result = (name != null ? name.hashCode() : 0);
        result = 37 * result + weight;
        result = 37 * result + heightInInches != +0.0f ? Float.floatToIntBits(heightInInches) : 0;
        result = 37 * result + (dateOfBirth != null ? dateOfBirth.hashCode() : 0);
        result = 37 * result + (bloodType != null ? bloodType.hashCode() : 0);
        return result;
    }
}

Now this is functional, but I only have 5 fields and this code already looks unwieldy… I might be going too far with unwieldy, but you know what I mean. Now with CL’s EqualsBuilder and HashCodeBuilder, the code quickly becomes more compact.


...
    public boolean equals(Object o) {
        Person otherPerson = (Person) o;
        return new EqualsBuilder()
                .append(name, otherPerson.name)
                .append(weight, otherPerson.weight)
                .append(heightInInches, otherPerson.heightInInches)
                .append(dateOfBirth, otherPerson.dateOfBirth)
                .append(bloodType, otherPerson.bloodType)
                .isEquals();
    }

    public int hashCode() {
        return new HashCodeBuilder(3, 37)
                .append(name)
                .append(weight)
                .append(heightInInches)
                .append(dateOfBirth)
                .append(bloodType)
                .toHashCode();
    }

Now I could make the equals() and hashCode() one-liners, but I am not interested in using the reflection-based methods on EqualsBuilder and HashCodeBuilder. Primarily because they are slower because well, they rely on reflection. Also, they cause issues on JVMs with certain security restrictions. So, using Java 1.5’s varargs and implicit autoboxing I came up with the equals/hashCode below.


...
    public boolean equals(Object obj) {
        Person otherPerson = (Person) obj;
        return CommonUtil.produceEquals(name, otherPerson.name,
                weight, otherPerson.weight,
                heightInInches, otherPerson.heightInInches,
                dateOfBirth, otherPerson.dateOfBirth,
                bloodType, otherPerson.bloodType);
    }

    public int hashCode() {
        return CommonUtil.produceHashCode(name, weight, heightInInches, dateOfBirth, bloodType);
    }

// Here are the methods in CommonUtil...
    public static int produceHashCode(final Object... itemsToHash) {
        int result = 0;
        if (isNonEmpty(itemsToHash)) {
            for (Object item : itemsToHash) {
                result = 37 * result + (item != null ? item.hashCode() : 0);
            }
        }
        return result;
    }

    public static boolean produceEquals(final Object... itemsToCompare) {
        if (isNonEmpty(itemsToCompare)) {
            if (0 == itemsToCompare.length % 2) {
                for (int index = 0; index < itemsToCompare.length; index += 2) {
                    final Object itemA = itemsToCompare[index];
                    final Object itemB = itemsToCompare[index + 1];
                    if (null == itemA ? null != itemB : !itemA.equals(itemB)) return false;
                }
            } else {
                throw new IllegalArgumentException("produceEquals() expects an itemsToCompare array of even length.");
            }
        }
        return true;
    }

I then ran test on the 3 variations and the default equals/hashCode from Object as a control. The test generated 100,000 People and placed them in a HashSet. The test runs 5 iterations per variation, and collects the avg times and heap deltas as benchmarks. The results below did not really surprise me. I knew the default native hashCode, and equals this == obj would be the fastest. However, they would not always be correct. If using an ORM, such as Hibernate, proper equals/hashCode implementions would almost be required (assuming you use collections). I expected the CL variation to take more time and memory than the default and per-variant, but I did not expect the custom variation to take just as much memory as CL. I assumed, since a new EqualsBuilder and HashCodeBuilder was built per call, the custom version would be a clear victor. Yeah, not so much. So, it simply comes down to using CL’s or my custom versions based on nothing other than whether or no not to add a new dependency. No major findings, but in this case, I’ll build my own.

Default Equals/HashCode
The avg time to generate and Add 100000 People to a Set: 2222ms (2.2 seconds)
The avg heap delta to generate and Add 100000 People to a Set: 1337563 bytes (1.28 MBs)

Per-variant specific Equals/HashCode
The avg time to generate and Add 100000 People to a Set: 2253ms (2.3 seconds)
The avg heap delta to generate and Add 100000 People to a Set: 1304939 bytes (1.24 MBs)

Commons-Lang Equals/HashCode
The avg time to generate and Add 100000 People to a Set: 2334ms (2.3 seconds)
The avg heap delta to generate and Add 100000 People to a Set: 5345507 bytes (5.10 MBs)

Custom Equals/HashCode (autoboxing)
The avg time to generate and Add 100000 People to a Set: 2331ms (2.3 seconds)
The avg heap delta to generate and Add 100000 People to a Set: 5226531 bytes (4.98 MBs)

Powered by WordPress with Pool theme design by Borja Fernandez.
Entries and comments feeds. Valid XHTML and CSS. ^Top^