Есть ли утилита отражения Java для глубокого сравнения двух объектов?

Я пытаюсь написать модульные тесты для различных операций clone() внутри большого проекта, и мне интересно, есть ли где-то существующий класс, способный принимать два объекта одного типа, делая глубокие сравнение, и если они идентичны или нет?

Ответ 1

Unitils имеет следующие функции:

Утверждение равенства через отражение, с различными вариантами, такими как игнорирование значений по умолчанию /null Java и игнорирование порядка коллекций

Ответ 2

Мне нравится этот вопрос! Главным образом потому, что он вряд ли когда-либо ответил или ответил плохо. Как будто никто еще не понял этого. Территория Богородицы:)

Во-первых, даже не думайте об использовании equals. Контракт equals, как определено в javadoc, является отношением эквивалентности (рефлексивным, симметричным и транзитивным), а не отношением равенства. Для этого он также должен быть антисимметричным. Единственной реализацией equals, которая (или когда-либо может быть), является истинное равенство равенства в java.lang.Object. Даже если вы использовали equals для сравнения всего на графике, риск взлома контракта довольно высок. Как отметил Джош Блох в "Эффективной Java", контракт на равных очень легко сломать:

"Просто невозможно расширить экземпляр класса и добавить аспект при сохранении равного контракта"

Кроме того, что хорошо делает логический метод на самом деле? Было бы неплохо на самом деле инкапсулировать все различия между оригиналом и клоном, не так ли? Кроме того, я предполагаю, что вы не хотите беспокоиться о написании/поддержании кода сравнения для каждого объекта на графике, но скорее вы ищете что-то, что будет масштабироваться с источником, поскольку оно изменяется со временем.

Soooo, то, что вы действительно хотите, это какой-то инструмент сравнения состояний. Как этот инструмент реализован, действительно зависит от характера вашей модели домена и ограничений производительности. По моему опыту, нет никакой универсальной магической пули. И он будет медленным при большом количестве итераций. Но для проверки полноты операции клонирования это будет хорошо работать. Ваши два лучших варианта - сериализация и отражение.

Некоторые проблемы, с которыми вы столкнетесь:

  • Порядок коллекции: следует ли рассматривать две коллекции, если они содержат одни и те же объекты, но в другом порядке?
  • Какие поля игнорировать: переходный? Статическая?
  • Типовая эквивалентность: должны ли значения полей быть одного типа? Или это нормально для того, чтобы расширить другой?
  • Там больше, но я забыл...

XStream довольно быстро и в сочетании с XMLUnit выполнит эту работу всего за несколько строк кода. XMLUnit хорош, потому что он может сообщать обо всех различиях или просто останавливаться при первом обнаружении. И его вывод включает в себя xpath для разных узлов, что приятно. По умолчанию он не разрешает неупорядоченные коллекции, но его можно настроить таким образом. Внедрение специального обработчика различий (Called a DifferenceListener) позволяет указать способ, которым вы хотите иметь дело с различиями, включая игнорирование порядка. Однако, как только вы захотите сделать что-либо, кроме простой настройки, становится трудно писать, и детали, как правило, привязаны к определенному объекту домена.

Мое личное предпочтение - использовать рефлексию для циклического перехода по всем объявленным полям и развернуть их в каждом, отслеживая различия по мере того, как я иду. Слово предупреждения: не используйте рекурсию, если вам не нравятся исключения. Держите вещи в области с помощью стека (используйте LinkedList или что-то еще). Обычно я игнорирую переходные и статические поля, и я пропускаю пары объектов, которые я уже сравнивал, поэтому я не заканчиваю бесконечные циклы, если кто-то решил написать самореференционный код (однако, я всегда сравниваю примитивные обертки, независимо от того, что, так как тот же объект refs часто используется повторно). Вы можете настроить вещи, чтобы игнорировать упорядочение коллекции и игнорировать специальные типы или поля, но мне нравится определять мои политики сравнения состояний в самих полях с помощью аннотаций. Это, ИМХО, является именно тем, для чего предназначались аннотации, чтобы метаданные о классе были доступны во время выполнения. Что-то вроде:


@StatePolicy(unordered=true, ignore=false, exactTypesOnly=true)
private List<StringyThing> _mylist;

Я думаю, что это действительно очень трудная проблема, но полностью разрешимая! И как только у вас есть что-то, что работает для вас, это действительно, действительно, удобно:)

Итак, удачи. И если вы придумаете что-то, что просто чистым гением, не забудьте поделиться!

Ответ 3

См. раздел DeepEquals и DeepHashCode() в java-util: https://github.com/jdereg/java-util

Этот класс выполняет именно то, что запрашивает первоначальный автор.

Ответ 4

Я использую XStream:

/**
 * @see java.lang.Object#equals(java.lang.Object)
 */
@Override
public boolean equals(Object o) {
    XStream xstream = new XStream();
    String oxml = xstream.toXML(o);
    String myxml = xstream.toXML(this);

    return myxml.equals(oxml);
}

/**
 * @see java.lang.Object#hashCode()
 */
@Override
public int hashCode() {
    XStream xstream = new XStream();
    String myxml = xstream.toXML(this);
    return myxml.hashCode();
}

Ответ 5

Просто нужно было реализовать сравнение двух экземпляров сущностей, пересмотренных с помощью Hibernate Envers. Я начал писать свои собственные, но нашел следующую структуру.

https://github.com/SQiShER/java-object-diff

Вы можете сравнить два объекта одного типа и отображать изменения, добавления и удаления. Если изменений нет, объекты равны (теоретически). Аннотации предоставляются для геттеров, которые следует игнорировать во время проверки. Работа с кадрами имеет гораздо более широкие области применения, чем проверка равенства, то есть я использую для создания журнала изменений.

Его производительность в порядке, при сравнении объектов JPA обязательно отключайте их от диспетчера сущности.

Ответ 6

http://www.unitils.org/tutorial-reflectionassert.html

public class User {

    private long id;
    private String first;
    private String last;

    public User(long id, String first, String last) {
        this.id = id;
        this.first = first;
        this.last = last;
    }
}
User user1 = new User(1, "John", "Doe");
User user2 = new User(1, "John", "Doe");
assertReflectionEquals(user1, user2);

Ответ 7

Пример вашего Linked List не так сложно обрабатывать. Когда код перемещается по двум объектным графам, он посещает объекты в Set или Map. Перед тем как перейти на другую ссылку на объект, этот набор проверяется, чтобы проверить, прошел ли объект уже пройден. Если это так, не нужно идти дальше.

Я согласен с человеком выше, который сказал, что использует LinkedList (например, Stack, но без синхронизированных методов на нем, поэтому он быстрее). Идеальным решением является перемещение графа объекта с использованием стека при использовании отражения для получения каждого поля. Написанный один раз, этот "внешний" equals() и "внешний" hashCode() - это то, что должны вызывать все методы equals() и hashCode(). Никогда еще вам не нужен метод equals() клиента.

Я написал немного кода, который пересекает полный граф объектов, указанный в Google Code. См. Json-io (http://code.google.com/p/json-io/). Он сериализует граф объектов Java в JSON и десериализуется из него. Он обрабатывает все объекты Java, с или без общих конструкторов, Serializeable или not Serializable и т.д. Этот же код обхода будет основой для внешней "equals()" и внешней реализации hashcode(). Btw, JsonReader/JsonWriter (json-io) обычно быстрее, чем встроенный объект ObjectInputStream/ObjectOutputStream.

Этот JsonReader/JsonWriter можно использовать для сравнения, но это не поможет с hashcode. Если вы хотите универсальный hashcode() и equals(), ему нужен собственный код. Возможно, я смогу это сделать с помощью общего посетителя графа. Посмотрим.

Другие соображения - статические поля - это легко - они могут быть пропущены, потому что все экземпляры equals() будут иметь одинаковое значение для статических полей, поскольку статические поля являются общими для всех экземпляров.

Что касается переходных полей - это будет выбираемый вариант. Иногда вам может потребоваться, чтобы переходные процессы не учитывали другие моменты. "Иногда вы чувствуете себя орехом, иногда вы этого не делаете".

Вернитесь к проекту json-io (для моих других проектов), и вы найдете внешний проект equals()/hashcode(). У меня пока нет имени, но это будет очевидно.

Ответ 8

Если ваши объекты реализуют Serializable, вы можете использовать это:

public static boolean deepCompare(Object o1, Object o2) {
    try {
        ByteArrayOutputStream baos1 = new ByteArrayOutputStream();
        ObjectOutputStream oos1 = new ObjectOutputStream(baos1);
        oos1.writeObject(o1);
        oos1.close();

        ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
        ObjectOutputStream oos2 = new ObjectOutputStream(baos2);
        oos2.writeObject(o2);
        oos2.close();

        return Arrays.equals(baos1.toByteArray(), baos2.toByteArray());
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

Ответ 9

Переопределить Метод equals()

Вы можете просто переопределить метод equals() класса с помощью EqualsBuilder.reflectionEquals(), как описано здесь:

 public boolean equals(Object obj) {
   return EqualsBuilder.reflectionEquals(this, obj);
 }

Ответ 10

Я думаю, вы знаете это, но в теории вы должны всегда переопределять .equals, чтобы утверждать, что два объекта действительно равны. Это означало бы, что они проверяют переопределенные методы .equals на своих членах.

Вот что такое .equals определено в Object.

Если бы это было сделано последовательно, у вас не было бы проблем.

Ответ 11

Гарантией прекращения такого глубокого сравнения может быть проблема. Что должны делать следующие? (Если вы реализуете такой компаратор, это создаст хороший unit test.)

LinkedListNode a = new LinkedListNode();
a.next = a;
LinkedListNode b = new LinkedListNode();
b.next = b;

System.out.println(DeepCompare(a, b));

Здесь другое:

LinkedListNode c = new LinkedListNode();
LinkedListNode d = new LinkedListNode();
c.next = d;
d.next = c;

System.out.println(DeepCompare(c, d));

Ответ 12

У Hamcrest есть Matcher samePropertyValuesAs. Но он опирается на Конвенцию JavaBeans (использует геттеры и сеттеры). Если объекты, которые должны быть сопоставлены, не имеют геттеров и сеттеров для своих атрибутов, это не сработает.

import static org.hamcrest.beans.SamePropertyValuesAs.samePropertyValuesAs;
import static org.junit.Assert.assertThat;

import org.junit.Test;

public class UserTest {

    @Test
    public void asfd() {
        User user1 = new User(1, "John", "Doe");
        User user2 = new User(1, "John", "Doe");
        assertThat(user1, samePropertyValuesAs(user2)); // all good

        user2 = new User(1, "John", "Do");
        assertThat(user1, samePropertyValuesAs(user2)); // will fail
    }
}

Пользователь bean - с геттерами и сеттерами

public class User {

    private long id;
    private String first;
    private String last;

    public User(long id, String first, String last) {
        this.id = id;
        this.first = first;
        this.last = last;
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getFirst() {
        return first;
    }

    public void setFirst(String first) {
        this.first = first;
    }

    public String getLast() {
        return last;
    }

    public void setLast(String last) {
        this.last = last;
    }

}

Ответ 13

Я думаю, что самым легким решением, вдохновленным решением Ray Hulha, является сериализация объекта, а затем глубокое сравнение исходного результата.

Сериализация может быть байтом, json, xml или простой toString и т.д. ToString кажется более дешевым. Lombok генерирует для нас бесплатный простой настраиваемый ToSTring. См. Пример ниже.

@ToString @Getter @Setter
class foo{
    boolean foo1;
    String  foo2;        
    public boolean deepCompare(Object other) { //for cohesiveness
        return other != null && this.toString().equals(other.toString());
    }
}   

Ответ 14

Apache дает вам что-то, конвертирует оба объекта в строку и сравнивает строки, но вам нужно переопределить toString()

obj1.toString().equals(obj2.toString())

Переопределить toString()

Если все поля являются примитивными типами:

import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
@Override
public String toString() {return 
ReflectionToStringBuilder.toString(this);}

Если у вас есть не примитивные поля и/или коллекция и/или карта:

// Within class
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
@Override
public String toString() {return 
ReflectionToStringBuilder.toString(this,new 
MultipleRecursiveToStringStyle());}

// New class extended from Apache ToStringStyle
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import java.util.*;

public class MultipleRecursiveToStringStyle extends ToStringStyle {
private static final int    INFINITE_DEPTH  = -1;

private int                 maxDepth;

private int                 depth;

public MultipleRecursiveToStringStyle() {
    this(INFINITE_DEPTH);
}

public MultipleRecursiveToStringStyle(int maxDepth) {
    setUseShortClassName(true);
    setUseIdentityHashCode(false);

    this.maxDepth = maxDepth;
}

@Override
protected void appendDetail(StringBuffer buffer, String fieldName, Object value) {
    if (value.getClass().getName().startsWith("java.lang.")
            || (maxDepth != INFINITE_DEPTH && depth >= maxDepth)) {
        buffer.append(value);
    } else {
        depth++;
        buffer.append(ReflectionToStringBuilder.toString(value, this));
        depth--;
    }
}

@Override
protected void appendDetail(StringBuffer buffer, String fieldName, 
Collection<?> coll) {
    for(Object value: coll){
        if (value.getClass().getName().startsWith("java.lang.")
                || (maxDepth != INFINITE_DEPTH && depth >= maxDepth)) {
            buffer.append(value);
        } else {
            depth++;
            buffer.append(ReflectionToStringBuilder.toString(value, this));
            depth--;
        }
    }
}

@Override
protected void appendDetail(StringBuffer buffer, String fieldName, Map<?, ?> map) {
    for(Map.Entry<?,?> kvEntry: map.entrySet()){
        Object value = kvEntry.getKey();
        if (value.getClass().getName().startsWith("java.lang.")
                || (maxDepth != INFINITE_DEPTH && depth >= maxDepth)) {
            buffer.append(value);
        } else {
            depth++;
            buffer.append(ReflectionToStringBuilder.toString(value, this));
            depth--;
        }
        value = kvEntry.getValue();
        if (value.getClass().getName().startsWith("java.lang.")
                || (maxDepth != INFINITE_DEPTH && depth >= maxDepth)) {
            buffer.append(value);
        } else {
            depth++;
            buffer.append(ReflectionToStringBuilder.toString(value, this));
            depth--;
        }
    }
}}