Java: A new approach to Equals()

by Daniel Winterstein published 16 October 2009

Custom equals() methods are handy but involve writing a lot of ugly boilerplate code - and there are booby-traps for the unwary. In this post, I present a way of over-riding equals() and hashCode() methods - based on annotations - which makes them a doddle to implement and maintain. The resulting code is available open-source on request in a shrink-wrapped jar.

To recap, a == b tests whether a and b are the same object. a.equals(b) tests whether they are equivalent objects. equals() is a very useful method, and you will no doubt have found yourself over-riding the default version in several of your classes, e.g. for use with sets and maps.

Over-riding equals() is not quite as straightforward as it seems. You must also override hashcode() in an equivalent manner. Otherwise HashSet, HashMap and HashTable will exhibit strange bugs. I've made this mistake in the past and can confirm it's confusing as hell to debug. Josh Bloch's Effective Java provides more examples of how equals can go wrong.

Also equals() and hashCode() methods contain a lot of boilerplate. Here's an example of equals() for a class - and this is from a simple class with only 2 fields:

public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        final ToyStoryState other = (ToyStoryStateobj;
        if (leftBank == null) {
            if (other.leftBank != null)
                return false;
        else if (!leftBank.equals(other.leftBank))
            return false;
        if (torch == null) {
            if (other.torch != null)
                return false;
        else if (!torch.equals(other.torch))
            return false;
        return true;
    }

Yuck! Ugly, barely readable and a great nesting place for bugs. To be fair to Java, equality is an inherently tricky issue. Once Ruby developer scoffed at this, and presented a "nicer" Ruby example -- which was much shorter, but it was also buggy. One reason why the code above is ugly is that it properly covers all the possibilities. Also, it was auto-generated by Eclipse rather than painfully written by hand.

Annotations to the rescue!

There is a nice solution to this issue using annotations. We'll start with an example, then look behind the scenes at how it works.

Example

class MyObject {
 
    @Equals
    public String name;
 
    private int id;
 
    @Equals
    public int getId() {
        return id;
    }
 
    public boolean equals(Object obj) {
        return Equality.equalsByAnnotation(MyObject.class, this, obj);
    }
 
    public int hashCode() {
        return Equality.hashCodeByAnnotation(MyObject.class, this);
    }
 
}

With the example above, two MyObjects are considered equal if they have the same name and id, as returned by getId(). Equals is our custom annotation, and Equality is a support class with static methods...

Making it work

The key to equals() is to know which properties are important. Properties can be fields or zero-argument methods, such as getters. This can be specified in a natural way by annotating fields and methods. First we define an annotation for properties that should be tested: @Equals indicates a property that should be compared using equals(). Defining an annotation is similar to defining an interface, except you use the keyword @interface, and you'll usually want to add some meta-annotations.

@Retention(RetentionPolicy.RUNTIME// Meta-annotation for "Don't throw this away during compilation"
@Target( { ElementType.FIELD, ElementType.METHOD }) // Meta-annotation for "Only allowed on fields and methods"
public @interface Equals { }

Now we can annotate properties like in the example - how do we test them? We need to make use of the reflection api. Given a Class object, we can get it's Field and Method objects via Class.getDeclaredFields/Methods(). Given those, we test for the presence of annotations using Field/Method.isAnnotationPresent(). We use a little known Java feature to get access to non-public fields. I put the code for all this is in a class Equality under the static methods equalsByAnnotation() and hashCodeByAnnotation().

Checking the superclass

Note that the equalsByAnnotation() method does not look at properties belonging to ancestor classes. Similarly, hashcodeByAnnotation() does not incorporate properties belonging to ancestor classes. If this is necessary, the user must call super.equals() and super.hashcode(). E.g. like this:

if super.equals(obj)) return false;
return equalsByAnnotation(MyObject.class, this, obj);

and

return 17 super.hashcode() * hashcodeByAnnotation(this);

This is a deliberate design decision. If the parent class has over-ridden equals(), then it's method (which may or may not use annotations) must be checked. If the parent class hasn;t over-ridden equals(), then it's method should be ignored, or your back at object idenity. I've left this as the user's responsibility - partly so they keep control, partly because the code to check for an ancestor equals method would have been ugly. If you think it should be handled automatically, feel free to send me your code suggestions...

Advantages

Shorter code, cleaner code, God kills less kittens, etc. Having @Equals attached to fields and methods makes it clear what is and isn't being tested. This helps in maintaining correct behaviour when the class is edited. And it isn't restrictive - you can still write your own custom code if you need to.

The Disadvantage: Speed

So the code is a lot nicer, but naturally you lose a bit of speed for using runtime lookups on fields. I did some time trials, and the annotations based method came out as 3x slower. That's fine during development and if equals() / hashCode() are not bottlenecks. Note that equals() and hashCode() can be bottlenecks, e.g. when making intensive use of Maps. So you may not want to use this in some production systems.

It has been suggested that I use bytecode editing (e.g. using ASM) to give a fast system. A very good idea, but sadly I'll have to leave that as an exercise for the reader for now.

The code for this article is licensed as open source code under LGPL, javadocs, and examples. Please let me know if you find this code useful.