Most users of Eclipse are familiar with its
code generation features. One of the more common of these is “Generate
hashCode() and equals()…” from the right-click -> Source (⌘-Opt-S)
menu. It roughly follows the recipe on page 33
of Joshua Bloch’s Effective Java, and it is effective in most situations.
One case where it doesn’t work is when the class is proxied.
Hibernate is just one example of a
framework that generates proxies. If the Person class below were
proxied, the default Eclipse implementation of equals()
would break.
In a Hibernate application, this can lead to anything from unnecessary
deletes and inserts to some very frustrating bugs.
public class Person {
private String name;
public String getName() {
return name;
}
}
The first potential problem with the default Eclipse equals()
is
object type. Proxy classes are generally subclasses, but they will never
be the same class as the object they’re proxying. As shown here, the
default Eclipse behavior is to check for type compatibility with
getClass(). If one of our Person objects were a proxy, its class
would not be Person, and this equals()
method would return
false. Fortunately Eclipse offers the option to use instanceof for the
type check, so all you have to do to solve this problem is click the
“Use ‘instanceof’ to compare types” checkbox.
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
DefaultPerson other = (DefaultPerson) obj;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
Let’s take a minute to talk about what proxies look like internally. The proxies generated by both cglib and Javassist will contain the same fields as the target class, and an actual instance of the target class will be held internally. Any method calls on the proxy are then delegated to this internal instance. Below is conceptually what a proxy looks like.
public class PersonProxy extends Person {
private String name; // <-- always null!
// the real Person
private Person person;
public String getName() {
// do some magic to get the real Person instance
return person.getName();
}
}
With this in mind, the next problem is the nullness of proxy fields. As
shown above, if a Person proxy were passed into the equals()
method
below, its name field would be null, and the method would return false.
The solution to this problem is to use a getter instead of field access.
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (!(obj instanceof Person))
return false;
Person other = (Person) obj;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
// ^^^^--- This will always be null!
return false;
return true;
}
The best way I’ve found to convert an equals()
method to use getters
is with the “Encapsulate Field…” refactoring. To do this, click on any
of the occurrences of name, then right-click on it (⌘-Opt-T) and
select “Encapsulate Field…” from the Refactoring menu. Make sure “use
setter and getter” is selected, and then click the “Preview” button.
If the class only has one field, you can then select both the
hashCode()
and equals()
methods from the “Refactored Source” pane
and copy them to the clipboard. Click the “Cancel” button and paste the
refactored versions over those currently in the class. If there are
multiple fields, you’ll have to encapsulate them all, then undo all of
the changes after copying the final method implementations. This
approach is less error prone than using find and replace, and it lets
you continue to use field access in methods other than hashCode()
and
equals()
.
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (!(obj instanceof Person))
return false;
Person other = (Person) obj;
if (getName() == null) {
if (other.getName() != null)
return false;
} else if (!getName().equals(other.getName()))
return false;
return true;
}
You should now have an equals()
method similar to the one above.
Because this version uses getters, the calls will always be delegated to
the real Person object, and you shouldn’t get any false equality
failures. You can certainly still have other issues with your equals()
implementation, but these two steps should address the main problems you
encounter when proxies enter the picture.