Skip to content

Commit

Permalink
DeepEquals.hashCode() is now sensitive to Array and List order.
Browse files Browse the repository at this point in the history
  • Loading branch information
jdereg committed Mar 11, 2024
1 parent 16ef960 commit f92dc4e
Show file tree
Hide file tree
Showing 3 changed files with 191 additions and 67 deletions.
104 changes: 72 additions & 32 deletions src/main/java/com/cedarsoftware/util/DeepEquals.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
Expand Down Expand Up @@ -652,7 +653,7 @@ private static boolean compareFloatingPointNumbers(Object a, Object b, double ep
}

/**
* Correctly handles floating point comparisions. <br>
* Correctly handles floating point comparisons. <br>
* source: http://floating-point-gui.de/errors/comparison/
*
* @param a first number
Expand Down Expand Up @@ -728,79 +729,118 @@ public static boolean hasCustomEquals(Class<?> c)
*
* This method will handle cycles correctly (A-&gt;B-&gt;C-&gt;A). In this case,
* Starting with object A, B, or C would yield the same hashCode. If an
* object encountered (root, suboject, etc.) has a hashCode() method on it
* object encountered (root, sub-object, etc.) has a hashCode() method on it
* (that is not Object.hashCode()), that hashCode() method will be called
* and it will stop traversal on that branch.
* @param obj Object who hashCode is desired.
* @return the 'deep' hashCode value for the passed in object.
*/
public static int deepHashCode(Object obj)
{
public static int deepHashCode(Object obj) {
Set<Object> visited = new HashSet<>();
return deepHashCode(obj, visited);
}

private static int deepHashCode(Object obj, Set<Object> visited) {
LinkedList<Object> stack = new LinkedList<>();
stack.addFirst(obj);
int hash = 0;

while (!stack.isEmpty())
{
while (!stack.isEmpty()) {
obj = stack.removeFirst();
if (obj == null || visited.contains(obj))
{
if (obj == null || visited.contains(obj)) {
continue;
}

visited.add(obj);

if (obj.getClass().isArray())
{
// Ensure array order matters to hash
if (obj.getClass().isArray()) {
final int len = Array.getLength(obj);
for (int i = 0; i < len; i++)
{
stack.addFirst(Array.get(obj, i));
long result = 1;

for (int i = 0; i < len; i++) {
Object element = Array.get(obj, i);
result = 31 * result + deepHashCode(element, visited); // recursive
}
hash += (int) result;
continue;
}

if (obj instanceof Collection)
{
stack.addAll(0, (Collection<?>)obj);
// Ensure list order matters to hash
if (obj instanceof List) {
List<?> list = (List<?>) obj;
long result = 1;

for (Object element : list) {
result = 31 * result + deepHashCode(element, visited); // recursive
}
hash += (int) result;
continue;
}

if (obj instanceof Map)
{
stack.addAll(0, ((Map<?, ?>)obj).keySet());
stack.addAll(0, ((Map<?, ?>)obj).values());
if (obj instanceof Collection) {
stack.addAll(0, (Collection<?>) obj);
continue;
}

if (obj instanceof Double || obj instanceof Float)
{
// just take the integral value for hashcode
// equality tests things more comprehensively
stack.add(Math.round(((Number) obj).doubleValue()));
if (obj instanceof Map) {
stack.addAll(0, ((Map<?, ?>) obj).keySet());
stack.addAll(0, ((Map<?, ?>) obj).values());
continue;
}

if (hasCustomHashCode(obj.getClass()))
{ // A real hashCode() method exists, call it.
// Protects Floats and Doubles from causing inequality, even if there are within an epsilon distance
// of one another. It does this by marshalling values of IEEE 754 numbers to coarser grained resolution,
// allowing for dynamic range on obviously different values, but identical values for IEEE 754 values
// that are near each other. Since hashes do not have to be unique, this upholds the hashCode()
// contract...two hash values that are not the same guarantee the objects are not equal, however, two
// values that are the same mean the two objects COULD be equals.
if (obj instanceof Float) {
hash += hashFloat((Float) obj);
continue;
} else if (obj instanceof Double) {
hash += hashDouble((Double) obj);
continue;
}

if (hasCustomHashCode(obj.getClass())) { // A real hashCode() method exists, call it.
hash += obj.hashCode();
continue;
}

Collection<Field> fields = ReflectionUtils.getDeepDeclaredFields(obj.getClass());
for (Field field : fields)
{
try
{
for (Field field : fields) {
try {
stack.addFirst(field.get(obj));
} catch (Exception ignored) {
}
catch (Exception ignored) { }
}
}
return hash;
}

private static final double SCALE_DOUBLE = Math.pow(10, 10);

private static int hashDouble(double value) {
// Normalize the value to a fixed precision
double normalizedValue = Math.round(value * SCALE_DOUBLE) / SCALE_DOUBLE;
// Convert to long for hashing
long bits = Double.doubleToLongBits(normalizedValue);
// Standard way to hash a long in Java
return (int)(bits ^ (bits >>> 32));
}

private static final float SCALE_FLOAT = (float)Math.pow(10, 5); // Scale according to epsilon for float

private static int hashFloat(float value) {
// Normalize the value to a fixed precision
float normalizedValue = Math.round(value * SCALE_FLOAT) / SCALE_FLOAT;
// Convert to int for hashing, as float bits can be directly converted
int bits = Float.floatToIntBits(normalizedValue);
// Return the hash
return bits;
}

/**
* Determine if the passed in class has a non-Object.hashCode() method. This
* method caches its results in static ConcurrentHashMap to benefit
Expand Down
66 changes: 62 additions & 4 deletions src/test/java/com/cedarsoftware/util/TestDeepEquals.java
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,19 @@ public void testPrimitiveArrays()
assertFalse(DeepEquals.deepEquals(array1, array4));
}

@Test
public void testArrayOrder()
{
int array1[] = { 3, 4, 7 };
int array2[] = { 7, 3, 4 };

int x = DeepEquals.deepHashCode(array1);
int y = DeepEquals.deepHashCode(array2);
assertNotEquals(x, y);

assertFalse(DeepEquals.deepEquals(array1, array2));
}

@Test
public void testOrderedCollection()
{
Expand All @@ -304,7 +317,27 @@ public void testOrderedCollection()
assertTrue(DeepEquals.deepEquals(x1, x2));
}

@Test
@Test
public void testOrderedDoubleCollection() {
List<Number> aa = asList(log(pow(E, 2)), tan(PI / 4));
List<Number> bb = asList(2.0, 1.0);
List<Number> cc = asList(1.0, 2.0);
assertEquals(DeepEquals.deepHashCode(aa), DeepEquals.deepHashCode(bb));
assertNotEquals(DeepEquals.deepHashCode(aa), DeepEquals.deepHashCode(cc));
assertNotEquals(DeepEquals.deepHashCode(bb), DeepEquals.deepHashCode(cc));
}

@Test
public void testOrderedFloatCollection() {
List<Number> aa = asList((float)log(pow(E, 2)), (float)tan(PI / 4));
List<Number> bb = asList(2.0f, 1.0f);
List<Number> cc = asList(1.0f, 2.0f);
assertEquals(DeepEquals.deepHashCode(aa), DeepEquals.deepHashCode(bb));
assertNotEquals(DeepEquals.deepHashCode(aa), DeepEquals.deepHashCode(cc));
assertNotEquals(DeepEquals.deepHashCode(bb), DeepEquals.deepHashCode(cc));
}

@Test
public void testUnorderedCollection()
{
Set<String> a = new HashSet<>(asList("one", "two", "three", "four", "five"));
Expand All @@ -317,10 +350,20 @@ public void testUnorderedCollection()
Set<Integer> d = new HashSet<>(asList(4, 2, 6));
assertFalse(DeepEquals.deepEquals(c, d));

Set<Class1> x1 = new HashSet<>(asList(new Class1(true, log(pow(E, 2)), 6), new Class1(true, tan(PI / 4), 1)));
Set<Class1> x2 = new HashSet<>(asList(new Class1(true, 1, 1), new Class1(true, 2, 6)));
assertTrue(DeepEquals.deepEquals(x1, x2));
Set<Class1> x1 = new LinkedHashSet<>();
x1.add(new Class1(true, log(pow(E, 2)), 6));
x1.add(new Class1(true, tan(PI / 4), 1));

Set<Class1> x2 = new HashSet<>();
x2.add(new Class1(true, 1, 1));
x2.add(new Class1(true, 2, 6));

int x = DeepEquals.deepHashCode(x1);
int y = DeepEquals.deepHashCode(x2);

assertEquals(x, y);
assertTrue(DeepEquals.deepEquals(x1, x2));

// Proves that objects are being compared against the correct objects in each collection (all objects have same
// hash code, so the unordered compare must handle checking item by item for hash-collided items)
Set<DumbHash> d1 = new LinkedHashSet<>();
Expand All @@ -341,6 +384,21 @@ public void testUnorderedCollection()
assert !DeepEquals.deepEquals(d2, d1);
}

@Test
public void testSetOrder() {
Set<String> a = new LinkedHashSet<>();
Set<String> b = new LinkedHashSet<>();
a.add("a");
a.add("b");
a.add("c");

b.add("c");
b.add("a");
b.add("b");
assertEquals(DeepEquals.deepHashCode(a), DeepEquals.deepHashCode(b));
assertTrue(DeepEquals.deepEquals(a, b));
}

@SuppressWarnings("unchecked")
@Test
public void testEquivalentMaps()
Expand Down
Loading

0 comments on commit f92dc4e

Please sign in to comment.