diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 61eca6880..5bfedf4ed 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -55,6 +55,7 @@ The type attribute can be add,update,fix,remove. ObjectUtils.identityToString(Object) and friends should allocate builders and buffers with a size EnumUtils.getEnumIgnoreCase and isValidEnumIgnoreCase methods added Add ToStringSummary annotation + Add bypass option for classes to recursive and reflective EqualsBuilder diff --git a/src/main/java/org/apache/commons/lang3/builder/EqualsBuilder.java b/src/main/java/org/apache/commons/lang3/builder/EqualsBuilder.java index d2cf7c7fb..60a532e02 100644 --- a/src/main/java/org/apache/commons/lang3/builder/EqualsBuilder.java +++ b/src/main/java/org/apache/commons/lang3/builder/EqualsBuilder.java @@ -19,8 +19,10 @@ package org.apache.commons.lang3.builder; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Field; import java.lang.reflect.Modifier; +import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; +import java.util.List; import java.util.Set; import org.apache.commons.lang3.ArrayUtils; @@ -213,6 +215,7 @@ public class EqualsBuilder implements Builder { private boolean testTransients = false; private boolean testRecursive = false; + private List> bypassReflectionClasses; private Class reflectUpToClass = null; private String[] excludeFields = null; @@ -223,7 +226,9 @@ public class EqualsBuilder implements Builder { * @see Object#equals(Object) */ public EqualsBuilder() { - // do nothing for now. + // set up default classes to bypass reflection for + bypassReflectionClasses = new ArrayList>(); + bypassReflectionClasses.add(String.class); //hashCode field being lazy but not transient } //------------------------------------------------------------------------- @@ -250,6 +255,23 @@ public class EqualsBuilder implements Builder { return this; } + /** + *

Set Classes whose instances should be compared by calling their equals + * although being in recursive mode. So the fields of theses classes will not be compared recursively by reflection.

+ * + *

Here you should name classes having non-transient fields which are cache fields being set lazily.
+ * Prominent example being {@link String} class with its hash code cache field. Due to the importance + * of the String class, it is included in the default bypasses classes. Usually, if you use + * your own set of classes here, remember to include String class, too.

+ * @param bypassReflectionClasses classes to bypass reflection test + * @return EqualsBuilder - used to chain calls. + * @since 3.8 + */ + public EqualsBuilder setBypassReflectionClasses(List> bypassReflectionClasses) { + this.bypassReflectionClasses = bypassReflectionClasses; + return this; + } + /** * Set the superclass to reflect up to at reflective tests. * @param reflectUpToClass the super class to reflect up to @@ -458,6 +480,10 @@ public class EqualsBuilder implements Builder { * *

Field names listed in field excludeFields will be ignored.

* + *

If either class of the compared objects is contained in + * bypassReflectionClasses, both objects are compared by calling + * the equals method of the left hand object with the right hand object as an argument.

+ * * @param lhs the left hand object * @param rhs the left hand object * @return EqualsBuilder - used to chain calls. @@ -503,10 +529,16 @@ public class EqualsBuilder implements Builder { if (testClass.isArray()) { append(lhs, rhs); } else { - reflectionAppend(lhs, rhs, testClass); - while (testClass.getSuperclass() != null && testClass != reflectUpToClass) { - testClass = testClass.getSuperclass(); + //If either class is being excluded, call normal object equals method on lhsClass. + if (bypassReflectionClasses != null + && (bypassReflectionClasses.contains(lhsClass) || bypassReflectionClasses.contains(rhsClass))) { + isEquals = lhs.equals(rhs); + } else { reflectionAppend(lhs, rhs, testClass); + while (testClass.getSuperclass() != null && testClass != reflectUpToClass) { + testClass = testClass.getSuperclass(); + reflectionAppend(lhs, rhs, testClass); + } } } } catch (final IllegalArgumentException e) { diff --git a/src/test/java/org/apache/commons/lang3/builder/EqualsBuilderTest.java b/src/test/java/org/apache/commons/lang3/builder/EqualsBuilderTest.java index 23651f612..05f1da9a8 100644 --- a/src/test/java/org/apache/commons/lang3/builder/EqualsBuilderTest.java +++ b/src/test/java/org/apache/commons/lang3/builder/EqualsBuilderTest.java @@ -168,6 +168,19 @@ public class EqualsBuilderTest { } } + static class TestRecursiveGenericObject { + + private final T a; + + TestRecursiveGenericObject(final T a) { + this.a = a; + } + + public T getA() { + return a; + } + } + static class TestRecursiveObject { private final TestRecursiveInnerObject a; private final TestRecursiveInnerObject b; @@ -418,6 +431,35 @@ public class EqualsBuilderTest { assertEquals(Boolean.TRUE, new EqualsBuilder().append((Object) null, null).build()); } + @Test + public void testObjectRecursiveGenericInteger() { + final TestRecursiveGenericObject o1_a = new TestRecursiveGenericObject(1); + final TestRecursiveGenericObject o1_b = new TestRecursiveGenericObject(1); + final TestRecursiveGenericObject o2 = new TestRecursiveGenericObject(2); + + assertTrue(new EqualsBuilder().setTestRecursive(true).append(o1_a, o1_b).isEquals()); + assertTrue(new EqualsBuilder().setTestRecursive(true).append(o1_b, o1_a).isEquals()); + + assertFalse(new EqualsBuilder().setTestRecursive(true).append(o1_b, o2).isEquals()); + } + + @Test + public void testObjectRecursiveGenericString() { + // Note: Do not use literals, because string literals are always mapped by same object (internal() of String))! + String s1_a = String.valueOf(1); + final TestRecursiveGenericObject o1_a = new TestRecursiveGenericObject(s1_a); + final TestRecursiveGenericObject o1_b = new TestRecursiveGenericObject(String.valueOf(1)); + final TestRecursiveGenericObject o2 = new TestRecursiveGenericObject(String.valueOf(2)); + + // To trigger bug reported in LANG-1356, call hashCode only on string in instance o1_a + s1_a.hashCode(); + + assertTrue(new EqualsBuilder().setTestRecursive(true).append(o1_a, o1_b).isEquals()); + assertTrue(new EqualsBuilder().setTestRecursive(true).append(o1_b, o1_a).isEquals()); + + assertFalse(new EqualsBuilder().setTestRecursive(true).append(o1_b, o2).isEquals()); + } + @Test public void testObjectRecursive() { final TestRecursiveInnerObject i1_1 = new TestRecursiveInnerObject(1);