2つのオブジェクトのフィールド値を全て比較する


備忘録です。

```java:DeepComparator.java

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.HashSet;
import java.util.stream.StreamSupport;

public class DeepComparator {

    public static boolean deepEquals(Object obj1, Object obj2) {
        return deepEquals(obj1, obj2, "", new IdentityHashMap<>());
    }

    private static boolean deepEquals(Object obj1, Object obj2, String path, Map<IdentityKey, String> visited) {
        if (obj1 == obj2) {
            return true;
        }
        if (obj1 == null || obj2 == null) {
            System.out.println("Null comparison failed at: " + path + " - " + (obj1 == null ? "obj1 is null" : "obj2 is null"));
            return false;
        }
        if (obj1.getClass() != obj2.getClass()) {
            System.out.println("Class comparison failed at: " + path + " - " + obj1.getClass().getName() + " vs " + obj2.getClass().getName());
            return false;
        }

        IdentityKey key1 = new IdentityKey(obj1);
        IdentityKey key2 = new IdentityKey(obj2);
        if (visited.containsKey(key1) || visited.containsKey(key2)) {
            System.out.println("Circular reference detected at: " + path);
            return true;
        }

        visited.put(key1, path);
        visited.put(key2, path);

        Class<?> clazz = obj1.getClass();
        while (clazz != null) {
            Field[] fields = clazz.getDeclaredFields();
            for (Field field : fields) {
                field.setAccessible(true);
                try {
                    Object value1 = field.get(obj1);
                    Object value2 = field.get(obj2);
                    String currentPath = path.isEmpty() ? field.getName() : path + "." + field.getName();
                    if (!compareValues(value1, value2, currentPath, visited)) {
                        System.out.println("Field comparison failed at: " + currentPath);
                        return false;
                    }
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                    return false;
                }
            }
            clazz = clazz.getSuperclass();
        }
        return true;
    }

    private static boolean compareValues(Object value1, Object value2, String path, Map<IdentityKey, String> visited) {
        if (value1 == value2) {
            return true;
        }
        if (value1 == null || value2 == null) {
            System.out.println("Null comparison failed at: " + path + " - " + (value1 == null ? "value1 is null" : "value2 is null"));
            return false;
        }
        if (value1.getClass().isArray() && value2.getClass().isArray()) {
            if (value1.getClass().getComponentType().isPrimitive()) {
                return primitiveArrayEquals(value1, value2, path);
            } else {
                return objectArrayEquals(value1, value2, path, visited);
            }
        } else if (value1 instanceof Iterable<?> && value2 instanceof Iterable<?>) {
            if (!deepEqualsIterable((Iterable<?>) value1, (Iterable<?>) value2, path, visited)) {
                System.out.println("Iterable comparison failed at: " + path);
                return false;
            }
        } else if (value1 instanceof Object && value2 instanceof Object) {
            if (Objects.equals(value1, value2)) {
                
                return true;
            }
            if (!deepEquals(value1, value2, path, visited)) {
                System.out.println("Object comparison failed at: " + path + " - " + value1 + " vs " + value2);
                return false;
            }
        } else {
            if (!Objects.equals(value1, value2)) {
                System.out.println("Value comparison failed at: " + path + " - " + value1 + " vs " + value2);
                return false;
            }
        }
        return true;
    }

    private static boolean primitiveArrayEquals(Object array1, Object array2, String path) {
        if (array1.getClass() != array2.getClass()) {
            return false;
        }

        int length1 = Array.getLength(array1);
        int length2 = Array.getLength(array2);

        if (length1 != length2) {
            System.out.println("Array length comparison failed at: " + path + " - " + length1 + " vs " + length2);
            return false;
        }

        for (int i = 0; i < length1; i++) {
            Object element1 = Array.get(array1, i);
            Object element2 = Array.get(array2, i);
            if (!Objects.equals(element1, element2)) {
                System.out.println("Array element comparison failed at: " + path + "[" + i + "] - " + element1 + " vs " + element2);
                return false;
            }
        }

        return true;
    }

    private static boolean objectArrayEquals(Object array1, Object array2, String path, Map<IdentityKey, String> visited) {
        Object[] objArray1 = (Object[]) array1;
        Object[] objArray2 = (Object[]) array2;

        if (objArray1.length != objArray2.length) {
            System.out.println("Array length comparison failed at: " + path + " - " + objArray1.length + " vs " + objArray2.length);
            return false;
        }

        for (int i = 0; i < objArray1.length; i++) {
            if (!deepEquals(objArray1[i], objArray2[i], path + "[" + i + "]", visited)) {
                System.out.println("Array element comparison failed at: " + path + "[" + i + "]");
                return false;
            }
        }

        return true;
    }

    private static boolean deepEqualsIterable(Iterable<?> iterable1, Iterable<?> iterable2, String path, Map<IdentityKey, String> visited) {
        if (iterable1 == iterable2) {
            return true;
        }
        if (iterable1 == null || iterable2 == null) {
            System.out.println("Null iterable comparison failed at: " + path + " - " + (iterable1 == null ? "iterable1 is null" : "iterable2 is null"));
            return false;
        }
        return deepEquals(toArray(iterable1), toArray(iterable2), path, visited);
    }

    private static Object[] toArray(Iterable<?> iterable) {
        return StreamSupport.stream(iterable.spliterator(), false)
                .toArray();
    }

    private static class IdentityKey {
        private final Object obj;

        IdentityKey(Object obj) {
            this.obj = obj;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            IdentityKey that = (IdentityKey) o;
            return obj == that.obj;
        }

        @Override
        public int hashCode() {
            return System.identityHashCode(obj);
        }
    }
}

```


Matcherを使う場合

```java:DeepEqualsMatcher.java

import org.hamcrest.Description;
import org.hamcrest.TypeSafeDiagnosingMatcher;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.stream.StreamSupport;

public class DeepEqualsMatcher extends TypeSafeDiagnosingMatcher<Object> {

    private final Object expected;

    public DeepEqualsMatcher(Object expected) {
        this.expected = expected;
    }

    @Override
    protected boolean matchesSafely(Object actual, Description mismatchDescription) {
        return deepEquals(expected, actual, "", new IdentityHashMap<>(), mismatchDescription);
    }

    @Override
    public void describeTo(Description description) {
        description.appendText("an object deeply equal to ").appendValue(expected);
    }

    private boolean deepEquals(Object obj1, Object obj2, String path, Map<IdentityKey, String> visited, Description mismatchDescription) {
        if (obj1 == obj2) {
            return true;
        }
        if (obj1 == null || obj2 == null) {
            mismatchDescription.appendText("Null comparison failed at: " + path + " - " + (obj1 == null ? "obj1 is null" : "obj2 is null"));
            return false;
        }
        if (obj1.getClass() != obj2.getClass()) {
            mismatchDescription.appendText("Class comparison failed at: " + path + " - " + obj1.getClass().getName() + " vs " + obj2.getClass().getName());
            return false;
        }

        IdentityKey key1 = new IdentityKey(obj1);
        IdentityKey key2 = new IdentityKey(obj2);
        if (visited.containsKey(key1) || visited.containsKey(key2)) {
            return true;
        }

        visited.put(key1, path);
        visited.put(key2, path);

        Class<?> clazz = obj1.getClass();
        while (clazz != null) {
            Field[] fields = clazz.getDeclaredFields();
            for (Field field : fields) {
                field.setAccessible(true);
                try {
                    Object value1 = field.get(obj1);
                    Object value2 = field.get(obj2);
                    String currentPath = path.isEmpty() ? field.getName() : path + "." + field.getName();
                    if (!compareValues(value1, value2, currentPath, visited, mismatchDescription)) {
                        mismatchDescription.appendText("Field comparison failed at: " + currentPath);
                        return false;
                    }
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                    return false;
                }
            }
            clazz = clazz.getSuperclass();
        }
        return true;
    }

    private boolean compareValues(Object value1, Object value2, String path, Map<IdentityKey, String> visited, Description mismatchDescription) {
        if (value1 == value2) {
            return true;
        }
        if (value1 == null || value2 == null) {
            mismatchDescription.appendText("Null comparison failed at: " + path + " - " + (value1 == null ? "value1 is null" : "value2 is null"));
            return false;
        }
        if (value1.getClass().isArray() && value2.getClass().isArray()) {
            if (value1.getClass().getComponentType().isPrimitive()) {
                return primitiveArrayEquals(value1, value2, path, mismatchDescription);
            } else {
                return objectArrayEquals(value1, value2, path, visited, mismatchDescription);
            }
        } else if (value1 instanceof Iterable<?> && value2 instanceof Iterable<?>) {
            if (!deepEqualsIterable((Iterable<?>) value1, (Iterable<?>) value2, path, visited, mismatchDescription)) {
                mismatchDescription.appendText("Iterable comparison failed at: " + path);
                return false;
            }
        } else if (value1 instanceof Object && value2 instanceof Object) {
            if (Objects.equals(value1, value2)) {
                
                return true;
            }
            if (!deepEquals(value1, value2, path, visited, mismatchDescription)) {
                mismatchDescription.appendText("Object comparison failed at: " + path + " - " + value1 + " vs " + value2);
                return false;
            }
        } else {
            if (!Objects.equals(value1, value2)) {
                mismatchDescription.appendText("Value comparison failed at: " + path + " - " + value1 + " vs " + value2);
                return false;
            }
        }
        return true;
    }

    private boolean primitiveArrayEquals(Object array1, Object array2, String path, Description mismatchDescription) {
        if (array1.getClass() != array2.getClass()) {
            return false;
        }

        int length1 = Array.getLength(array1);
        int length2 = Array.getLength(array2);

        if (length1 != length2) {
            mismatchDescription.appendText("Array length comparison failed at: " + path + " - " + length1 + " vs " + length2);
            return false;
        }

        for (int i = 0; i < length1; i++) {
            Object element1 = Array.get(array1, i);
            Object element2 = Array.get(array2, i);
            if (!Objects.equals(element1, element2)) {
                mismatchDescription.appendText("Array element comparison failed at: " + path + "[" + i + "] - " + element1 + " vs " + element2);
                return false;
            }
        }

        return true;
    }

    private boolean objectArrayEquals(Object array1, Object array2, String path, Map<IdentityKey, String> visited, Description mismatchDescription) {
        Object[] objArray1 = (Object[]) array1;
        Object[] objArray2 = (Object[]) array2;

        if (objArray1.length != objArray2.length) {
            mismatchDescription.appendText("Array length comparison failed at: " + path + " - " + objArray1.length + " vs " + objArray2.length);
            return false;
        }

        for (int i = 0; i < objArray1.length; i++) {
            if (!deepEquals(objArray1[i], objArray2[i], path + "[" + i + "]", visited, mismatchDescription)) {
                mismatchDescription.appendText("Array element comparison failed at: " + path + "[" + i + "]");
                return false;
            }
        }

        return true;
    }

    private boolean deepEqualsIterable(Iterable<?> iterable1, Iterable<?> iterable2, String path, Map<IdentityKey, String> visited, Description mismatchDescription) {
        if (iterable1 == iterable2) {
            return true;
        }
        if (iterable1 == null || iterable2 == null) {
            mismatchDescription.appendText("Null iterable comparison failed at: " + path + " - " + (iterable1 == null ? "iterable1 is null" : "iterable2 is null"));
            return false;
        }
        return deepEquals(toArray(iterable1), toArray(iterable2), path, visited, mismatchDescription);
    }

    private Object[] toArray(Iterable<?> iterable) {
        return StreamSupport.stream(iterable.spliterator(), false)
                .toArray();
    }

    private static class IdentityKey {
        private final Object obj;

        IdentityKey(Object obj) {
            this.obj = obj;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            IdentityKey that = (IdentityKey) o;
            return obj == that.obj;
        }

        @Override
        public int hashCode() {
            return System.identityHashCode(obj);
        }
    }

    public static DeepEqualsMatcher deepEqualsTo(Object expected) {
        return new DeepEqualsMatcher(expected);
    }
}
```