Many times I have seen HashCodeBuilder and EqualsBuilder getting used in the Models to write hashCode() and equals() method. In SDEs like eclipse, help is just one click away. But for many reasons, one might not resort to this option because you have to keep updating your equals and hashCode methods when you are adding new variables to the class.
It is important to note that these builders use reflection to access the variables.
The important point to notice here is that the superclass fields will be included.
I know it is too tempting to use these builders especially when you want to ignore certain fields for equals().
For instance, you would not want to compare a enriched field such as "Account" you would want to restrict the equals() to only "accountID"
When you are over-using this utility with ignoreFields, you run into the following problem.
It is important to note that these builders use reflection to access the variables.
It uses AccessibleObject.setAccessible to gain access to private fields. This means that it will throw a security exception if run under a security manager, if the permissions are not set up correctly. It is also not as efficient as testing explicitly.
Transient members will be not be tested, as they are likely derived fields, and not part of the value of the Object.
Static fields will not be tested. Superclass fields will be included.
The important point to notice here is that the superclass fields will be included.
I know it is too tempting to use these builders especially when you want to ignore certain fields for equals().
For instance, you would not want to compare a enriched field such as "Account" you would want to restrict the equals() to only "accountID"
When you are over-using this utility with ignoreFields, you run into the following problem.
import org.apache.commons.lang.builder.EqualsBuilder; import org.apache.commons.lang.builder.HashCodeBuilder; public class BaseClass { public static String[] ignoreFieldsInEquals = new String[] {"comments"}; private String id; private String comments; public BaseClass() { super(); } public BaseClass(String id, String comments) { super(); this.id = id; this.comments = comments; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getComments() { return comments; } public void setComments(String comments) { this.comments = comments; } @Override public int hashCode() { return HashCodeBuilder.reflectionHashCode(this, ignoreFieldsInEquals); } @Override public boolean equals(Object obj) { return EqualsBuilder.reflectionEquals(this, obj, ignoreFieldsInEquals); } }
package com.foo; import org.apache.commons.lang.builder.EqualsBuilder; import org.apache.commons.lang.builder.HashCodeBuilder; public class SubClass extends BaseClass { private static String[] ignoreFieldsInEquals = new String[] {"alias"}; private String name; private String alias; public SubClass() { super(); } public SubClass(String id, String comments,String name, String alias) { super(id, comments); this.name = name; this.alias = alias; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAlias() { return alias; } public void setAlias(String alias) { this.alias = alias; } @Override public int hashCode() { return super.hashCode() + HashCodeBuilder.reflectionHashCode(this, ignoreFieldsInEquals); } @Override public boolean equals(Object obj) { return super.equals(obj) && EqualsBuilder.reflectionEquals(this, obj, ignoreFieldsInEquals); } }
import static org.junit.Assert.*; import org.junit.Test; public class ReflectionBuilderTest { @Test public void test() { String id1 = "TE1"; SubClass sub1 = new SubClass(id1, "random comment 1", "Bruce Wayne", "batsy"); SubClass sub2 = new SubClass(id1, "random comment 2", "Bruce Wayne", "batman"); assertEquals(sub1, sub2); //fails } }
In SubClass, we are expecting super.equals() to ignore comments from BaseClass and the equals to ignore alias but it will not work since it uses reflection.
so EqualsBuilder.reflectionEquals() already read your alias and returned false. One way to fix this would be, to pass the base class's ignore field as well in the SubClass's equals.
BaseClass :
private static String[] ignoreFieldsInEquals = new String[] {"comments"}; @Override public int hashCode() { return HashCodeBuilder.reflectionHashCode(this, getFieldsToIgnoreInEquals()); } @Override public boolean equals(Object obj) { return EqualsBuilder.reflectionEquals(this, obj, getFieldsToIgnoreInEquals()); } protected String[] getFieldsToIgnoreInEquals() { return ignoreFieldsInEquals; }
SubClass:
private static String[] ignoreFieldsInEquals = new String[] {"alias"}; protected String[] getFieldsToIgnoreInEquals() { return Stream.of(ignoreFieldsInEquals, super.getFieldsToIgnoreInEquals()).flatMap(Stream::of).toArray(String[]::new); } @Override public int hashCode() { return HashCodeBuilder.reflectionHashCode(this, getFieldsToIgnoreInEquals()); } @Override public boolean equals(Object obj) { return EqualsBuilder.reflectionEquals(this, obj, getFieldsToIgnoreInEquals()); }
But even this would not work when the sub class has to choose what BaseClass's variables to ignore and what not to ignore.
For example, there are many sub classes for the BaseClass and one of them decides that comments are also important for them. Usually such need does not arrive for equals() rather for something like "isValueEqualsForPersist"
The ideal solution would be a careful engineering about where to put what. In this case, for example, maybe the comments should not even be in the BaseClass.
But this post is more about how much reflection can screw it up. For Example I am working on a 10 year old project where Equals and HashCode builders are used and there are sub classes, where one of them needs a super class fields in equalsForPersist and the other does not. Now refactoring the whole thing and restructuring the classes is a big pain in the ass, which could have been avoided by careful designing.
No comments:
Post a Comment