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


99

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


1
Как этот класс узнает, может ли он в определенной точке графов объектов принимать идентичные объекты или только одни и те же ссылки?
Зед

В идеале он будет достаточно настраиваемым :) Я ищу что-то автоматическое, чтобы при добавлении (а не клонировании) новых полей тест мог их идентифицировать.
Uri

3
Я пытаюсь сказать, что вам все равно нужно будет настроить (т.е. реализовать) сравнения. Так почему бы не переопределить метод equals в своих классах и не использовать его?
Зед

3
Если equals возвращает false для большого сложного объекта, с чего начать? Намного лучше превратить объект в многострочную строку и выполнить сравнение строк. Тогда вы сможете точно увидеть, где два объекта отличаются. IntelliJ открывает окно сравнения «изменений», которое помогает найти несколько изменений между двумя результатами, то есть он понимает вывод assertEquals (string1, string2) и дает вам окно сравнения.
Питер Лоури,

Здесь есть несколько действительно хороших ответов, помимо принятого, которые, кажется, уже похоронены
user1445967

Ответы:


63

Unitils имеет такую ​​функциональность:

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


9
Я провел некоторое тестирование этой функции и, похоже, провел глубокое сравнение, в отличие от EqualsBuilder.
Говард может

Есть ли способ не игнорировать переходные поля?
Pinch

@ Пинч, я тебя слышу. Я бы сказал, что инструмент глубокого сравнения unitilsошибочен именно потому, что он сравнивает переменные, даже если они могут не иметь заметного влияния . Еще одно (нежелательное) последствие сравнения переменных заключается в том, что чистые замыкания (без собственного состояния) не поддерживаются. Кроме того, он требует, чтобы сравниваемые объекты относились к одному и тому же типу среды выполнения. Я закатал рукава и создал свою собственную версию инструмента глубокого сравнения, которая решает эти проблемы.
beluchin

@Wolfgang, есть ли какой-нибудь образец кода, к которому мы можем обратиться? Откуда вы взяли эту цитату?
anon58192932

30

Мне нравится этот вопрос! В основном потому, что на него почти никогда не отвечают или плохо отвечают. Вроде еще никто не понял. Девственная территория :)

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

«Просто нет способа расширить инстанцируемый класс и добавить аспект, сохранив при этом контракт равенства»

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

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

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

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

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

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


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

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

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


15

См. DeepEquals и DeepHashCode () в java-util: https://github.com/jdereg/java-util

Этот класс делает именно то, что требует оригинальный автор.


4
Предупреждение: DeepEquals использует метод .equals () объекта, если он существует. Возможно, это не то, что вам нужно.
Adam

4
Он использует .equals () в классе только в том случае, если метод equals () был явно добавлен, в противном случае он выполняет сравнение по элементам. Логика здесь в том, что если кто-то попытался написать собственный метод equals (), то его следует использовать. Будущее усовершенствование: разрешить флаг, чтобы он игнорировал методы equals (), даже если они существуют. В java-util есть полезные утилиты, например CaseInsensitiveMap / Set.
John DeRegnaucourt

Меня беспокоит сравнение полей. Различие в полях может быть незаметным с точки зрения клиента объектов, и все же глубокое сравнение на основе полей отметит это. Кроме того, для сравнения полей требуется, чтобы объекты были одного и того же типа среды выполнения, что может быть ограничивающим.
beluchin

Чтобы ответить на @beluchin выше, DeepEquals.deepEquals () не всегда выполняет сравнение поля за полем. Во-первых, у него есть возможность использовать .equals () для метода, если он существует (не тот, который есть в Object), или его можно игнорировать. Во-вторых, при сравнении карт / коллекций он не смотрит ни на тип коллекции или карты, ни на поля в коллекции / карте. Вместо этого он сравнивает их логически. LinkedHashMap может быть равен TreeMap, если они имеют одинаковое содержимое и элементы в одинаковом порядке. Для неупорядоченных коллекций и карт требуются только элементы размера и глубокого равенства.
John DeRegnaucourt

при сравнении карт / коллекций он не смотрит ни на тип коллекции или карты, ни на поля в коллекции / карте. Вместо этого он сравнивает их логически @JohnDeRegnaucourt, я бы сказал, что это логическое сравнение, то есть сравнение только того, что publicдолжно применяться ко всем типам, в отличие от применимости только к коллекции / картам.
beluchin

10

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

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

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

7

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

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

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

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


6

Я использую 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
Коллекции, отличные от списков, могут возвращать элементы в другом порядке, поэтому сравнение строк не удастся.
Алексей Березкин

Также классы, которые не сериализуемы, будут терпеть неудачу
Zwelch

6

В AssertJ вы можете:

Assertions.assertThat(expectedObject).isEqualToComparingFieldByFieldRecursively(actualObject);

Вероятно, это не будет работать во всех случаях, однако это будет работать в большем количестве случаев, чем вы думаете.

Вот что говорится в документации:

Утверждение, что тестируемый объект (фактический) равен заданному объекту на основе рекурсивного свойства / поля путем сравнения свойств / полей (включая унаследованные). Это может быть полезно, если фактическая реализация equals вам не подходит. Рекурсивное сравнение свойств / полей не применяется к полям, имеющим настраиваемую реализацию равенства, т. Е. Вместо поля при сравнении полей будет использоваться переопределенный метод равенства.

Рекурсивное сравнение обрабатывает циклы. По умолчанию значения с плавающей запятой сравниваются с точностью 1.0E-6 и удваиваются с точностью 1.0E-15.

Вы можете указать настраиваемый компаратор для (вложенных) полей или типа с помощью соответственно usingComparatorForFields (Comparator, String ...) и usingComparatorForType (Comparator, Class).

Сравниваемые объекты могут быть разных типов, но должны иметь одинаковые свойства / поля. Например, если фактический объект имеет поле имени String, ожидается, что другой объект также будет иметь его. Если у объекта есть поле и свойство с тем же именем, значение свойства будет использоваться над полем.


1
isEqualToComparingFieldByFieldRecursivelyтеперь устарела. Используйте assertThat(expectedObject).usingRecursiveComparison().isEqualTo(actualObject);вместо этого :)
даргмуесли

5

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);

2
особенно полезно, если вам нужно обрабатывать сгенерированные классы, в которых вы не имеете никакого влияния на равенство!
Matthias B

1
stackoverflow.com/a/1449051/829755 уже упоминал об этом. Вы должны были отредактировать это сообщение
user829755

1
@ user829755 Таким образом я теряю очки. ТАК все про точечную игру)) Людям нравится получать кредиты за проделанную работу, я тоже.
gavenkoa

3

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
    }
}

Пользовательский компонент - с геттерами и сеттерами

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;
    }

}

Это прекрасно работает до тех пор, пока у вас не будет POJO, использующего isFooметод чтения для Booleanсвойства. Есть PR, который открыт с 2016 года, чтобы исправить это. github.com/hamcrest/JavaHamcrest/pull/136
Snekse

2

Если ваши объекты реализуют 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);
    }
}

1

Ваш пример связанного списка не так уж и сложен. По мере того, как код проходит по двум графам объектов, он помещает посещенные объекты в набор или карту. Перед переходом к другой ссылке на объект этот набор проверяется, чтобы увидеть, был ли уже пройден объект. Если да, то дальше идти не надо.

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

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

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

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

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

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


1

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--;
        }
    }
}}

0

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

Именно поэтому .equals определен в Object.

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


2
Проблема в том, что я хочу автоматизировать тестирование этой большой существующей кодовой базы, которую я не писал ... :)
Ури

0

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

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));

Если у вас есть новый вопрос, задайте его, нажав кнопку « Задать вопрос» . Включите ссылку на этот вопрос, если это помогает понять контекст.
YoungHobbit

@younghobbit: нет, это не новый вопрос. Вопросительный знак в ответе не делает этот флаг подходящим. Пожалуйста, обратите внимание.
Бен Фойгт

Отсюда: Using an answer instead of a comment to get a longer limit and better formatting.Если это комментарий, то зачем использовать раздел ответов? Вот почему я отметил это. не из-за ?. Этот ответ уже отмечен кем-то другим, который не оставил комментарий. Я только что получил это в очереди на рассмотрение. Могло быть мое бедствие в том, что я должен был быть более осторожным.
YoungHobbit

0

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

Сериализация может быть байтовой, 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());
    }
}   

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.