備忘録です。
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を使う場合
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);
}
}