Czy istnieje narzędzie do refleksji w języku Java umożliwiające dokładne porównanie dwóch obiektów?


99

Próbuję napisać testy jednostkowe dla różnych clone()operacji w dużym projekcie i zastanawiam się, czy istnieje gdzieś istniejąca klasa, która jest w stanie pobrać dwa obiekty tego samego typu, przeprowadzić głębokie porównanie i powiedzieć, czy one są identyczne czy nie?


1
Skąd ta klasa miałaby wiedzieć, czy w pewnym punkcie grafów obiektów może akceptować identyczne obiekty, czy tylko te same odwołania?
Zed

Idealnie byłoby wystarczająco konfigurowalne :) Szukam czegoś automatycznego, aby w przypadku dodania (a nie sklonowania) nowych pól test mógł je zidentyfikować.
Uri

3
Próbuję powiedzieć, że i tak będziesz musiał skonfigurować (tj. Zaimplementować) porównania. Dlaczego więc nie zastąpić metody equals w swoich klasach i jej użyć?
Zed

3
Jeśli equals zwraca false dla dużego złożonego obiektu, od czego zacząć? Znacznie lepiej jest zamienić obiekt w wieloliniowy łańcuch i wykonać porównanie typu String. Wtedy możesz zobaczyć dokładnie, gdzie dwa obiekty są różne. IntelliJ wyświetla okno porównania "zmian", które pomaga znaleźć wiele zmian między dwoma wynikami, tj. Rozumie dane wyjściowe funkcji assertEquals (string1, string2) i wyświetla okno porównania.
Peter Lawrey,

Jest tu kilka naprawdę dobrych odpowiedzi, poza tą zaakceptowaną, które wydają się zostać pogrzebane
user1445967

Odpowiedzi:


63

Unitils ma tę funkcjonalność:

Potwierdzenie równości poprzez odbicie, z różnymi opcjami, takimi jak ignorowanie wartości domyślnych / null języka Java i ignorowanie kolejności kolekcji


9
Zrobiłem kilka testów tej funkcji i wydaje się, że robię głębokie porównanie, podczas gdy EqualsBuilder tego nie robi.
Howard Maj

Czy istnieje sposób, aby nie ignorować pól przejściowych?
Pinch

@Pinch, słyszę cię. Powiedziałbym, że narzędzie do głębokiego porównywania unitilsjest wadliwe właśnie dlatego, że porównuje zmienne, nawet jeśli nie mają one zauważalnego wpływu . Inną (niepożądaną) konsekwencją porównywania zmiennych jest to, że czyste domknięcia (bez własnego stanu) nie są obsługiwane. Ponadto wymaga, aby porównywane obiekty były tego samego typu środowiska wykonawczego. Zakasałem rękawy i stworzyłem własną wersję narzędzia do głębokiego porównywania, które rozwiązuje te problemy.
beluchin

@Wolfgang czy jest jakiś przykładowy kod, do którego można nas skierować? Skąd wziąłeś ten cytat?
anon58192932,

30

Uwielbiam to pytanie! Głównie dlatego, że prawie nigdy nie ma na nie odpowiedzi ani odpowiedzi źle. To tak, jakby nikt jeszcze tego nie rozgryzł. Dziewicze terytorium :)

Po pierwsze, nawet nie myśl o używaniu equals. Kontrakt equals, zgodnie z definicją w javadoc, jest relacją równoważności (zwrotną, symetryczną i przechodnią), a nie relacją równości. W tym celu musiałby być również antysymetryczny. Jedyną implementacją equalstego jest (lub kiedykolwiek mogłaby być) prawdziwa relacja równości to ta w java.lang.Object. Nawet jeśli kiedyś equalsporównywałeś wszystko na wykresie, ryzyko zerwania umowy jest dość wysokie. Jak zauważył Josh Bloch w Effective Java , kontrakt równych jest bardzo łatwy do zerwania:

„Po prostu nie ma sposobu na rozszerzenie instancji klasy i dodanie aspektu przy zachowaniu kontraktu równości”

Poza tym, po co tak naprawdę metoda boolowska? Nie sądzisz, że byłoby miło ująć wszystkie różnice między oryginałem a klonem? Zakładam również, że nie chcesz przejmować się pisaniem / utrzymywaniem kodu porównawczego dla każdego obiektu na wykresie, ale raczej szukasz czegoś, co będzie skalowane wraz ze źródłem, gdy zmienia się w czasie.

Soooo, to, czego naprawdę chcesz, to jakieś narzędzie do porównywania stanów. Sposób implementacji tego narzędzia zależy w rzeczywistości od charakteru modelu domeny i ograniczeń wydajności. Z mojego doświadczenia wynika, że ​​nie ma ogólnej magicznej kuli. I to będzie to powolne przez dużą liczbę iteracji. Ale jeśli chodzi o testowanie kompletności operacji klonowania, wykona to zadanie całkiem dobrze. Dwie najlepsze opcje to serializacja i odbicie.

Niektóre problemy, które napotkasz:

  • Kolejność kolekcji: czy dwie kolekcje należy uważać za podobne, jeśli zawierają te same przedmioty, ale w innej kolejności?
  • Które pola ignorować: przejściowe? Statyczny?
  • Równoważność typów: czy wartości pól powinny być dokładnie tego samego typu? A może jedno może przedłużyć drugie?
  • Jest więcej, ale zapominam ...

XStream jest dość szybki i w połączeniu z XMLUnit wykona zadanie w zaledwie kilku wierszach kodu. XMLUnit jest fajny, ponieważ może zgłosić wszystkie różnice lub po prostu zatrzymać się na pierwszym, który znajdzie. A jego dane wyjściowe obejmują ścieżkę xpath do różnych węzłów, co jest miłe. Domyślnie nie zezwala na nieuporządkowane kolekcje, ale można je tak skonfigurować. Wstrzyknięcie specjalnego modułu obsługi różnic (nazywanego aDifferenceListener modułu ) pozwala określić sposób radzenia sobie z różnicami, w tym ignorowanie kolejności. Jednak gdy tylko zechcesz zrobić cokolwiek poza najprostszym dostosowaniem, napisanie staje się trudne, a szczegóły są zwykle powiązane z określonym obiektem domeny.

Osobiście wolę używać refleksji, aby przeglądać wszystkie zadeklarowane pola i analizować każde z nich, śledząc różnice. Słowo ostrzeżenia: nie używaj rekursji, chyba że lubisz wyjątki przepełnienia stosu. Trzymaj rzeczy w zakresie ze stosem (użyj plikuLinkedListlub coś). Zwykle ignoruję pola przejściowe i statyczne oraz pomijam pary obiektów, które już porównałem, więc nie kończę w nieskończonych pętlach, jeśli ktoś zdecydował się napisać samoodwołujący się kod (jednak zawsze porównuję prymitywne opakowania bez względu na wszystko , ponieważ te same referencje obiektów są często używane ponownie). Możesz skonfigurować rzeczy z góry, aby ignorować porządkowanie kolekcji i ignorować specjalne typy lub pola, ale ja lubię definiować moje zasady porównywania stanów na samych polach za pomocą adnotacji. Właśnie do tego, IMHO, służyły adnotacje, aby metadane o klasie były dostępne w czasie wykonywania. Coś jak:


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

Myślę, że to naprawdę trudny problem, ale całkowicie możliwy do rozwiązania! A kiedy już masz coś, co działa dla Ciebie, jest to naprawdę bardzo przydatne :)

Więc powodzenia. A jeśli wymyślisz coś, co jest po prostu geniuszem, nie zapomnij o tym!


15

Zobacz DeepEquals i DeepHashCode () w java-util: https://github.com/jdereg/java-util

Ta klasa robi dokładnie to, o co prosi pierwotny autor.


4
Ostrzeżenie: DeepEquals używa metody .equals () obiektu, jeśli taki istnieje. To może nie być to, czego chcesz.
Adam,

4
Używa .equals () tylko dla klasy, jeśli metoda equals () została jawnie dodana, w przeciwnym razie wykonuje porównanie składowe. Logika jest taka, że ​​jeśli ktoś podjął próbę napisania niestandardowej metody equals (), to powinien zostać użyty. Przyszłe ulepszenia: pozwól fladze ignorować metody equals (), nawet jeśli istnieją. W java-util są przydatne narzędzia, takie jak CaseInsensitiveMap / Set.
John DeRegnaucourt

Martwię się porównywaniem pól. Różnica pól może nie być zauważalna z punktu widzenia klienta obiektu, a mimo to głębokie porównanie oparte na polach oznaczałoby to. Ponadto porównywanie pól wymaga, aby obiekty były tego samego typu w czasie wykonywania, co może być ograniczające.
beluchin

Aby odpowiedzieć na @beluchin powyżej, DeepEquals.deepEquals () nie zawsze przeprowadza porównanie pola po polu. Po pierwsze, ma opcję użycia .equals () w metodzie, jeśli taka istnieje (nie jest tą w Object), lub można ją zignorować. Po drugie, porównując mapy / kolekcje, nie bierze pod uwagę typu zbioru lub mapy ani pól w kolekcji / mapie. Zamiast tego porównuje je logicznie. LinkedHashMap może równać się z TreeMap, jeśli mają tę samą zawartość i elementy w tej samej kolejności. W przypadku niezorganizowanych kolekcji i map wymagane są tylko elementy o rozmiarze i głęboko równości.
John DeRegnaucourt

podczas porównywania map / kolekcji nie uwzględnia typu zbioru lub mapy ani pól w kolekcji / mapie. Zamiast tego porównuje je logicznie @JohnDeRegnaucourt, argumentowałbym, że to logiczne porównanie, tj. Porównując tylko to, co publicpowinno mieć zastosowanie do wszystkich typów, a nie tylko do kolekcji / map.
beluchin

10

Zastąp metodę equals ()

Możesz po prostu nadpisać metodę equals () klasy za pomocą EqualsBuilder.reflectionEquals (), jak wyjaśniono tutaj :

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

7

Wystarczyło zaimplementować porównanie dwóch instancji encji poprawionych przez Hibernate Envers. Zacząłem pisać własne różnice, ale potem znalazłem następujące ramy.

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

Możesz porównać dwa obiekty tego samego typu i pokaże zmiany, uzupełnienia i usunięcia. Jeśli nie ma zmian, obiekty są równe (w teorii). Dla metod pobierających, które powinny być ignorowane podczas sprawdzania, są dostępne adnotacje. Ramka ma znacznie szersze zastosowania niż sprawdzanie równości, tj. Używam do generowania dziennika zmian.

Jego działanie jest OK, porównując jednostki JPA, pamiętaj, aby najpierw odłączyć je od zarządzającego podmiotami.


6

Używam 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
Kolekcje inne niż listy mogą zwracać elementy w innej kolejności, więc porównanie ciągów zakończy się niepowodzeniem.
Alexey Berezkin

Również klasy, których nie można serializować, zawiodą
Zwelch

6

W AssertJ możesz:

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

Prawdopodobnie nie będzie działać we wszystkich przypadkach, jednak będzie działać w większej liczbie przypadków, niż myślisz.

Oto, co mówi dokumentacja:

Potwierdź, że testowany obiekt (rzeczywisty) jest równy danemu obiektowi w oparciu o rekursywne porównanie właściwości / pola przez właściwość / pole (w tym dziedziczone). Może to być przydatne, jeśli implementacja rzeczywistego równa się Ci nie odpowiada. Rekurencyjne porównanie właściwość / pole nie jest stosowane w przypadku pól z niestandardową implementacją równa się, tzn. Zamiast pola przez porównanie zostanie użyta metoda przesłoniętej równości.

Porównanie rekurencyjne obsługuje cykle. Domyślnie zmiennoprzecinkowe są porównywane z dokładnością 1,0E-6 i podwajają się z 1,0E-15.

Możesz określić niestandardowy komparator dla (zagnieżdżonych) pól lub typu, używając odpowiednio usingComparatorForFields (Comparator, String ...) i usingComparatorForType (Comparator, Class).

Obiekty do porównania mogą być różnych typów, ale muszą mieć te same właściwości / pola. Na przykład, jeśli rzeczywisty obiekt ma nazwę Pole typu String, oczekuje się, że drugi obiekt również będzie miał taką nazwę. Jeśli obiekt ma pole i właściwość o tej samej nazwie, wartość właściwości zostanie użyta nad polem.


1
isEqualToComparingFieldByFieldRecursivelyjest teraz przestarzała. Użyj assertThat(expectedObject).usingRecursiveComparison().isEqualTo(actualObject);zamiast tego :)
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
szczególnie przydatne, jeśli masz do czynienia z wygenerowanymi klasami, na które nie masz wpływu na równe sobie!
Matthias B

1
stackoverflow.com/a/1449051/829755 już o tym wspomniał. powinieneś edytować ten post
user829755

1
@ user829755 W ten sposób tracę punkty. Więc wszystko o grze punktowej)) Ludzie lubią dostać kredyty za wykonaną pracę, ja też.
gavenkoa

3

Hamcrest ma Matcher samePropertyValuesAs . Ale opiera się na Konwencji JavaBeans (używa metod pobierających i ustawiających). Jeżeli obiekty, które mają być porównywane, nie mają metod pobierających i ustawiających dla swoich atrybutów, to nie zadziała.

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

Fasola użytkownika - z metodami pobierającymi i ustawiającymi

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

}

Działa to świetnie, dopóki nie masz POJO, który używa isFoometody odczytu dla Booleanwłaściwości. Istnieje PR, który jest otwarty od 2016 roku, aby to naprawić. github.com/hamcrest/JavaHamcrest/pull/136
Snekse

2

Jeśli Twoje obiekty implementują Serializable, możesz użyć tego:

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

Twój przykład listy połączonej nie jest trudny w obsłudze. Gdy kod przechodzi przez dwa wykresy obiektów, umieszcza odwiedzane obiekty w zestawie lub mapie. Przed przejściem do innego odniesienia do obiektu, ten zestaw jest testowany, aby sprawdzić, czy obiekt już przeszedł. Jeśli tak, nie musisz iść dalej.

Zgadzam się z osobą powyżej, która powiedziała, że ​​użyj LinkedList (jak Stack, ale bez zsynchronizowanych metod, więc jest szybszy). Idealnym rozwiązaniem jest przechodzenie przez wykres obiektu za pomocą stosu, podczas gdy odbicia są używane do uzyskania każdego pola. Napisane raz, to „zewnętrzne” equals () i „zewnętrzne” hashCode () są tym, co powinny wywoływać wszystkie metody equals () i hashCode (). Nigdy więcej nie potrzebujesz metody customer equals ().

Napisałem kawałek kodu, który przechodzi przez kompletny graf obiektów, wymieniony w Google Code. Zobacz json-io (http://code.google.com/p/json-io/). Serializuje graf obiektu Java do formatu JSON i deserializuje z niego. Obsługuje wszystkie obiekty Java, z lub bez publicznych konstruktorów, z możliwością serializacji lub bez możliwości serializacji itp. Ten sam kod przechodzenia będzie podstawą dla zewnętrznej implementacji „equals ()” i zewnętrznej implementacji „hashcode ()”. Przy okazji, JsonReader / JsonWriter (json-io) jest zwykle szybszy niż wbudowany ObjectInputStream / ObjectOutputStream.

Tego JsonReader / JsonWriter można użyć do porównania, ale nie pomoże to z hashcode. Jeśli chcesz mieć uniwersalny hashcode () i equals (), potrzebuje własnego kodu. Może uda mi się to zrobić z ogólnym odwiedzającym wykres. Zobaczymy.

Inne kwestie - pola statyczne - to proste - można je pominąć, ponieważ wszystkie wystąpienia equals () miałyby tę samą wartość dla pól statycznych, ponieważ pola statyczne są współużytkowane przez wszystkie wystąpienia.

Jeśli chodzi o pola przejściowe - będzie to opcja do wyboru. Czasami możesz chcieć, aby stany nieustalone liczyły się, innym razem nie. „Czasami czujesz się jak wariat, czasami nie”.

Wróć do projektu json-io (dla moich innych projektów), a znajdziesz zewnętrzny projekt equals () / hashcode (). Nie mam jeszcze na to nazwy, ale będzie to oczywiste.


1

Apache daje ci coś, konwertuje oba obiekty na ciągi porównuje ciągi, ale musisz Override toString ()

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

Zastąp toString ()

Jeśli wszystkie pola są typami pierwotnymi:

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

Jeśli masz inne niż pierwotne pola i / lub kolekcje i / lub mapy:

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

Myślę, że wiesz o tym, ale teoretycznie powinieneś zawsze nadpisywać .equals, aby zapewnić, że dwa obiekty są naprawdę równe. Oznaczałoby to, że sprawdzają nadpisane metody .equals na swoich członkach.

Właśnie z tego powodu .equals jest zdefiniowane w Object.

Gdyby to było robione konsekwentnie, nie miałbyś problemu.


2
Problem polega na tym, że chcę zautomatyzować testowanie tego dla dużej istniejącej bazy kodów, której nie napisałem ... :)
Uri

0

Gwarancja zatrzymania tak głębokiego porównania może stanowić problem. Co powinny zrobić następujące osoby? (Jeśli zaimplementujesz taki komparator, będzie to dobry test jednostkowy).

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

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

Oto kolejny:

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

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

Jeśli masz nowe pytanie, zadaj je, klikając przycisk Zadaj pytanie . Dołącz link do tego pytania, jeśli pomaga to w dostarczeniu kontekstu.
YoungHobbit

@younghobbit: nie, to nie jest nowe pytanie. Znak zapytania w odpowiedzi nie oznacza, że ​​flaga jest odpowiednia. Proszę, poświęć więcej uwagi.
Ben Voigt,

Z tego: Using an answer instead of a comment to get a longer limit and better formatting.Jeśli to jest komentarz, to po co używać sekcji odpowiedzi? Dlatego oznaczyłem to. nie z powodu ?. Ta odpowiedź jest już oznaczona przez kogoś innego, kto nie zostawił komentarza. Właśnie dostałem to w kolejce do recenzji. Mogłem być zły, że powinienem był być bardziej ostrożny.
YoungHobbit

0

Myślę, że najłatwiejsze rozwiązanie inspirowane rozwiązaniem Ray Hulha jest serializacja obiektu, a następnie dokładne porównanie surowego wyniku.

Serializacja może być albo bajtem, jsonem, xml lub prostym toStringiem itp. ToString wydaje się tańsze. Lombok generuje dla nas darmowe, łatwe do dostosowania pierścienie ToST. Zobacz przykład poniżej.

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

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.