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

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)

This entry was posted in Technical stuff. Bookmark the permalink.

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

  1. Brian says:

    You think you can add some exit points form you equals method? That code is HOT!!! LOL…jerky!!!

  2. jaybose says:

    Umm, thx. I removed the extra “return false;” after the exception.

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

    I like the code to only have one exit in it because it makes the code easier to test and debug. When you have multiple exit points you are adding complexity and can find youself getting a result you may not have expected. Above is how I would refactor your produceEquals method.

  4. Brian says:

    One of the things I’ve been contemplating is what works best in a helper class while what should be in a parent class. Overriding equals() and hashCode() seem to me like something you would want to do for all of your domain objects. Is there reason you put this in it’s own class?

  5. jaybose says:

    I agree, those 2 methods should be in all domain objects. However, I have non-domain objects that rely on equals/hashCode functionality. So, similar to having an external HashCodeBuilder or EqualsBuilder, you need this code in a centralized yet accessible module. Hence, the CommonUtil.

  6. Brian says:

    Ok, you have non-domain objects that need to have equals and hashcode??? Oh please explain!!!

    Also that original hashcode() is poop. Looks like some autogen code that will f-up in a HashSet. I tired that out in 1.4 and created a set of objects. I then looped through the list, modifiying one field and then did a check if the set contained the modified object. In all cases the modified object was not found.

  7. jaybose says:

    No, as it turns out (after lengthy discussion w/ Jason C, 3 days i think), that is correct behavior.

    I also looked at the HasSet implementation, and they store the hashcode of the object as you add it to the collection in a map of hashcodes -> Objects.

    The only way to gurantee that your object is found is to provide a immutable hashcode to your objects upon creation. Jason gives more info on that.

  8. Sony says:

    OR:
    Add all your fields to an ArrayList and return list.equals() or list.hashcode(). See contract for java.util.List.

  9. jaybose says:

    Sony, I wish that Sets worked that way.

    I assume storing the hashcode rather than calculating it at request time was based on performance. Not sure.

  10. Sony says:

    i meant use a List to implement equals() & hashcode() “instead” of CommonUtil.produceXXX.

    Regarding HashSet Impl – i have other thoughts – maybe for another time.

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.