Чи є утиліта відображення Java для глибокого порівняння двох об'єктів?


99

Я намагаюся писати одиничні тести для різних clone()операцій у великому проекті, і мені цікаво, чи існує десь клас, який здатний приймати два об'єкти одного типу, робити глибоке порівняння і говорити, чи вони ти однаковий чи ні?


1
Як би цей клас знав, чи може в певному пункті об'єктних графіків приймати однакові об'єкти чи лише ті самі посилання?
Зед

В ідеалі це буде досить конфігуруватися :) Я шукаю щось автоматичне, щоб, якщо нові поля були додані (а не були клоновані), тест міг їх ідентифікувати.
Урі

3
Я намагаюся сказати, що вам потрібно буде налаштувати (тобто реалізувати) порівняння в будь-якому випадку. То чому б тоді не замінити метод рівних у своїх класах і використовувати це?
Зед

3
Якщо дорівнює повертає значення false для великого складного об'єкта, з чого починати? Вам набагато краще перетворити об’єкт на багаторядковий рядок і зробити порівняння String. Тоді ви можете точно бачити, де два об’єкти різні. IntelliJ спливає вікно порівняння "зміни", яке допомагає знайти кілька змін між двома результатами, тобто воно розуміє вихідний файл assertEquals (string1, string2) і дає вікно порівняння.
Пітер Лорі

Тут є кілька справді хороших відповідей, окрім прийнятих, які, схоже, потрапили в поховання
user1445967

Відповіді:


62

Unitils має цю функціональність:

Затвердження рівності за допомогою рефлексії, з різними варіантами, такими як ігнорування значень за замовчуванням / нуль Java та ігнорування порядку колекцій


9
Я провів тестування на цій функції, і, схоже, я робив глибоке порівняння, де, як EqualsBuilder, цього немає.
Говард Травень

Чи є спосіб, щоб це не ігнорувало перехідні поля?
Пінч

@Pinch Я тебе чую. Я б сказав, що інструмент глибокого порівняння у unitilsпрограші є недосконалим саме тому, що він порівнює змінні, навіть коли вони можуть не мати помітного впливу . Іншим (небажаним) наслідком порівняння змінних є те, що чисті закриття (без власного стану) не підтримуються. Крім того, він вимагає, щоб порівняні об'єкти були одного типу. Я закатав рукави і створив власну версію інструменту глибокого порівняння, який вирішує ці проблеми.
белучин

@Wolfgang Чи є зразок коду, щоб направити нас? Звідки ви взяли цю цитату?
anon58192932

30

Я люблю це питання! Головним чином, тому, що на нього навряд чи можна відповісти погано. Наче ніхто ще не зрозумів цього. Діва територія :)

По-перше, навіть не думайте про використання equals. Договір equals, як визначено в javadoc, - це відношення еквівалентності (рефлексивне, симетричне та перехідне), а не відношення рівності. Для цього це також мало би бути антисиметричним. Єдине втілення equalsцього - це (або коли-небудь могло б бути) справжнього відношення рівності - це те, що в java.lang.Object. Навіть якщо ви equalsпорівнювали все на графіку, ризик розірвати контракт досить високий. Як вказував Джош Блох в « Ефективній Java» , контракт рівних дуже просто розірвати:

"Просто немає способу розширити клас миттєвих даних і додати аспект при збереженні контракту на рівні"

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

Soooo, те, що ви дійсно хочете, це якийсь інструмент порівняння стану. Те, як цей інструмент реалізований, дійсно залежить від характеру вашої доменної моделі та ваших обмежень на продуктивність. На мій досвід, немає жодної родової магічної кулі. І це буде повільно через велику кількість ітерацій. Але для перевірки повноти операції з клонуванням це зробить роботу досить добре. Ваші два найкращі варіанти - серіалізація та рефлексія.

Деякі проблеми, з якими ви зіткнетесь:

  • Порядок колекції: чи слід вважати дві колекції схожими, якщо вони містять однакові предмети, але в іншому порядку?
  • Які поля ігнорувати: Тимчасові? Статичний?
  • Еквівалентність типу: чи мають значення поля точно такого ж типу? Або нормально, щоб один продовжував інший?
  • Є більше, але я забуваю ...

XStream досить швидкий і в поєднанні з XMLUnit зробить роботу лише за кілька рядків коду. XMLUnit приємний тим, що він може повідомити про всі відмінності або просто зупинитися на першому, який знайде. І його вихід включає xpath до різних вузлів, що добре. За замовчуванням він не дозволяє невпорядковані колекції, але це можна налаштувати для цього. Введення спеціального обробника різниці (Called a DifferenceListener) дозволяє вказати спосіб вирішення розбіжностей, включаючи ігнорування порядку. Однак, як тільки ви хочете зробити що-небудь за межі найпростішого налаштування, писати стає важко, і деталі, як правило, прив'язуються до конкретного об’єкта домену.

Мої особисті переваги - використовувати рефлексію, щоб переглядати всі задекларовані поля та переглядати кожне з них, відстежуючи відмінності, як я йду. Слово попередження: Не використовуйте рекурсії, якщо вам не подобаються винятки переповнення стека. Зберігайте речі в обсязі стеком (використовуйте aLinkedListабо щось). Зазвичай я ігнорую перехідні та статичні поля, і пропускаю об'єктні пари, які я вже порівняв, тому я не опиняюся у нескінченних петлях, якщо хтось вирішив написати самореференційний код (Однак я завжди порівнюю примітивні обгортки незалежно від того, що , оскільки одні і ті ж реф. об'єкти часто повторно використовуються). Ви можете налаштувати речі наперед, щоб ігнорувати впорядкування колекції та ігнорувати спеціальні типи чи поля, але мені подобається визначати мою політику порівняння стану на самих полях через анотації. Саме для цього, 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 () об'єкта, якщо такий існує. Це може бути не те, що ви хочете.
Адам

4
Він використовує лише .equals () для класу, якщо явно був доданий метод equals (), інакше він робить порівняння по кожному члену. Логіка тут полягає в тому, що якщо хтось доклав зусиль, щоб написати користувальницький метод equals (), то його слід використовувати. Майбутнє вдосконалення: дозвольте прапорцеві ігнорувати методи equals (), навіть якщо вони існують. У Java-util є такі корисні утиліти, як CaseInsensitiveMap / Set.
Джон Де Реньокур

Я хвилююся за порівняння полів. Різниця в полях може бути не помітна з точки зору клієнта об'єктів, і все ж глибоке порівняння на основі полів позначило б це. Крім того, для порівняння полів потрібно, щоб об'єкти були одного типу виконання, які можуть бути обмежуючими.
белучин

Щоб відповісти на @beluchin вище, DeepEquals.deepEquals () не завжди проводить порівняння по кожному полю. По-перше, у нього є можливість використовувати .equals () для методу, якщо такий існує (тобто не той, що знаходиться на Object), або його можна ігнорувати. По-друге, при порівнянні карт / колекцій він не дивиться на тип колекції чи карти, ані поля на колекції / карті. Натомість порівнює їх логічно. LinkedHashMap може дорівнювати TreeMap, якщо вони мають однаковий вміст і елементи в одному порядку. Для колекцій, які не впорядковані, і карти, потрібні лише предмети розміру та глибокі рівні.
Джон Де Реньокур

порівнюючи Карти / Колекції, він не розглядає тип колекції чи карти, ані поля колекції / карти. Натомість порівнює їх логічно @JohnDeRegnaucourt, я б заперечував це логічне порівняння, тобто порівнюючи лише те, що publicмає застосовуватися до всіх типів, на відміну від того, що стосується лише колекції / карт.
белучин

10

Перевизначення Метод рівних ()

Ви можете просто змінити метод 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
Колекції, окрім списків, можуть повертати елементи в іншому порядку, тому порівняння рядків не вдасться.
Олексій Березкін

Також не вдасться
пройти

6

У AssertJ ви можете:

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

Напевно, це не спрацює у всіх випадках, однак воно буде працювати у більшості випадків, про які ви думаєте.

Ось що говорить документація:

Стверджують, що досліджуваний об'єкт (фактичний) дорівнює даному об'єкту на основі рекурсивної властивості / поля за порівнянням властивостей / полів (включаючи спадкові). Це може бути корисно, якщо реальна реалізація не відповідає вам. Рекурсивне порівняння властивостей / полів не застосовується для полів, що мають власну реалізацію рівних, тобто метод замінення рівних буде використаний замість порівняння поля за полем.

Рекурсивне порівняння обробляє цикли. За замовчуванням поплавці порівнюються з точністю до 1,0Е-6 і подвоюються з 1,0Е-15.

Ви можете вказати спеціальний компаратор для (вкладених) полів або типу, використовуючи відповідноComparatorForFields (Comparator, String ...) та використовуючиComparatorForType (Comparator, Class).

Об'єкти для порівняння можуть бути різних типів, але повинні мати однакові властивості / поля. Наприклад, якщо фактичний об'єкт має ім'я String, очікується, що інший об'єкт також має його. Якщо об’єкт має поле та властивість з тим самим іменем, значення властивості буде використано над полем.


1
isEqualToComparingFieldByFieldRecursivelyзараз застаріло. Використовуйте assertThat(expectedObject).usingRecursiveComparison().isEqualTo(actualObject);замість цього :)
dargmuesli

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
особливо корисно, якщо вам доведеться обробляти згенеровані класи, де ви не маєте жодного впливу на рівні!
Маттіас Б

1
stackoverflow.com/a/1449051/829755 вже згадував про це. ви мали б відредагувати цю публікацію
user829755

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

3

У Hamcrest є Matcher той же самийPropertyValuesAs . Але він покладається на Конвенцію 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 (як Stack, але без синхронізованих методів на ньому, так це швидше). Переміщення графіку об'єкта за допомогою стека, використовуючи відображення для отримання кожного поля, є ідеальним рішенням. Написаний один раз, цей "зовнішній" дорівнює () і "зовнішній" hashCode () - це те, що всі методи рівняння () та hashCode () повинні викликати. Ніколи більше не потрібен метод клієнта рівний ().

Я написав трохи коду, який проходить повний графік об'єкта, перелічений у Google Code. Див. Json-io (http://code.google.com/p/json-io/). Він серіалізує об’єктний графік Java в JSON і десеріалізується з нього. Він обробляє всі об’єкти Java, з або без публічних конструкторів, Serializeable чи не Serializable тощо. Цей самий код траверсу буде основою для зовнішньої реалізації "equals ()" та external "hashcode ()". До речі, JsonReader / JsonWriter (json-io) зазвичай швидший, ніж вбудований ObjectInputStream / ObjectOutputStream.

Цей JsonReader / JsonWriter можна використовувати для порівняння, але він не допоможе з хеш-кодом. Якщо вам потрібно універсальний хеш-код () і дорівнює (), йому потрібен власний код. Можливо, мені вдасться вирішити це із загальним відвідувачем графіків. Побачимо.

Інші міркування - статичні поля - це просто - їх можна пропустити, тому що всі еквівалентні екземпляри () мали б однакове значення для статичних полів, оскільки статичні поля поділяються у всіх екземплярах.

Щодо перехідних полів - це буде вибір. Іноді ви можете хотіти, щоб перехідні періоди рахували інший раз. "Іноді ти відчуваєш себе горіхом, іноді - ні."

Поверніться до проекту json-io (для інших моїх проектів), і ви знайдете проект зовнішніх рівних () / хеш-код (). Я ще не маю назви цього, але це буде очевидно.


1

Apache щось дає, перетворити обидва об'єкти в рядки та порівняти рядки, але вам доведеться переосмислити toString ()

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

Заміна наString ()

Якщо всі поля примітивні типи:

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
Проблема полягає в тому, що я хочу автоматизувати тестування на великій існуючій кодовій базі, яку я не писав ... :)
Uri

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.