Saturday, October 29, 2005

Early Binding Gotchas

I recently ran into code like this:

public class MyType {
public boolean equals(Object other) {
if (other == null) return false;
if (other == this) return true;
if (!(other instanceof MyType)) return false;
return equals((MyType)other);
}
public boolean equals(MyType other) {
return someValue == other.someValue;
}
}

Nothing really stands out as being WRONG, does it? It's making clever use of overridden methods and it looks like it might even save us some time if we know the object ahead of time (equals(MyType)). And that is where the "gotcha" is! Let's look at a test shall we?

public void testEquals() {
MyType myType=createExampleMyType();
MyType otherType=createAnotherExampleMyType();
MyType nullMyType=null;
Object nullObject=null;

assertTrue(myType.equals(myType)); //correct
assertFalse(myType.equals(null)); //correct
assertFalse(myType.equals(otherType)); //correct
assertFalse(myType.equals(nullObject)); //correct
assertFalse(myType.equals(nullMyType)); //This thows anException! WHAT THE @#$%^&!!
}

OK, let's break this down shall we? The first test binds to equals(MyType) because the java compiler early binds its arguments in message sends. It figures out the argument types from the variable types used and finds the appropriate message to call at run-time (basically, it early binds the arguments and late binds the receiver). The second test binds to equals(Object) because it finds the most generic method it can. Since, we didn't say what type of null to use, the compiler picked Object for us. Next up, the third test binds just like the first, since we know the types of arguments. The fourth test is now trivial to us, right? Which leads us to the final and most troublesome test. Bascially, equals(MyType) got bound because the compiler doesn't know that we left that good ole null in there! So, where we thought we were safe, we are not. We just bypassed all our null checking code. Oh poo...

So, what can you do?

public class MyType {
public boolean equals(Object other) {
if (other == this) return true;
if (!(other instanceof MyType)) return false;
return equals((MyType)other);
}
public boolean equals(MyType other) {
if (other == null) return false;
return someValue == other.someValue;
}
}

The simple answer is to move your null check into your equals(MyType) method and be done with it. Just remember that null and early binding are gotchas not to be taken lightly when using overridden methods.

2 comments:

Jeff said...

Have you looked at Jakarta Commons EqualsBuilder? The JavaDocs describes this as typical usage:
public boolean equals(Object obj) {
if (obj instanceof MyClass == false) {
return false;
}
if (this == obj) {
return true;
}
MyClass rhs = (MyClass) obj;
return new EqualsBuilder()
.appendSuper(super.equals(obj))
.append(field1, rhs.field1)
.append(field2, rhs.field2)
.append(field3, rhs.field3)
.isEquals();
}

Jeff said...

That should be a link to http://jakarta.apache.org/commons/lang/api/org/apache/commons/lang/builder/EqualsBuilder.html

Amazon