Существует ли утилита отражения Java для глубокого сравнения двух объектов? - PullRequest
87 голосов
/ 19 сентября 2009

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

Ответы [ 15 ]

60 голосов
/ 19 сентября 2009

Unitils обладает такой функциональностью:

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

27 голосов
/ 30 сентября 2010

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

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

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

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

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

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

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

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

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


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

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

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

13 голосов
/ 26 апреля 2012

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

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

7 голосов
/ 05 сентября 2017

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

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

 public boolean equals(Object obj) {
   return EqualsBuilder.reflectionEquals(this, obj);
 }
7 голосов
/ 17 сентября 2012

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

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

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

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

6 голосов
/ 30 сентября 2009

Я использую 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();
}
3 голосов
/ 23 июля 2018

В AssertJ , вы можете сделать:

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

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

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

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

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

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

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

3 голосов
/ 12 февраля 2013

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 голосов
/ 05 октября 2015

Если ваши объекты реализуют 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 голос
/ 26 сентября 2017

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