Marc Schwieterman

That depends...

Eclipse Generated Equality

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.

1
2
3
4
5
6
7
8
9
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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.

1
2
3
4
5
6
7
8
9
10
11
12
13
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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.