Esiste un'utilità di riflessione Java per eseguire un confronto approfondito di due oggetti?


99

Sto cercando di scrivere unit test per una varietà di clone()operazioni all'interno di un grande progetto e mi chiedo se esiste una classe esistente da qualche parte che è in grado di prendere due oggetti dello stesso tipo, fare un confronto profondo e dire se sei identico o no?


1
Come farebbe questa classe a sapere se a un certo punto degli oggetti grafici può accettare oggetti identici o solo gli stessi riferimenti?
Zed

Idealmente sarà abbastanza configurabile :) Sto cercando qualcosa di automatico in modo che se vengono aggiunti nuovi campi (e non clonati) il test possa identificarli.
Uri

3
Quello che sto cercando di dire è che dovrai comunque configurare (cioè implementare) i confronti. Allora perché non sovrascrivere il metodo uguale nelle tue classi e usarlo?
Zed

3
Se è uguale a restituisce false per un oggetto complesso di grandi dimensioni, da dove inizi? È molto meglio trasformare l'oggetto in una stringa multilinea e fare un confronto di stringhe. Quindi puoi vedere esattamente dove due oggetti sono diversi. IntelliJ fa apparire una finestra di confronto delle "modifiche" che aiuta a trovare più modifiche tra due risultati, cioè comprende l'output di assertEquals (stringa1, stringa2) e fornisce una finestra di confronto.
Peter Lawrey

Ci sono alcune risposte davvero buone qui, oltre a quella accettata, che sembrano essere state sepolte
user1445967

Risposte:


62

Unitils ha questa funzionalità:

Asserzione di uguaglianza attraverso la riflessione, con diverse opzioni come ignorare i valori predefiniti / nulli di Java e ignorare l'ordine delle raccolte


9
Ho fatto alcuni test su questa funzione e sembra fare un confronto approfondito dove come EqualsBuilder non lo fa.
Howard maggio

C'è un modo per fare in modo che questo non ignori i campi transitori?
Pizzico

@ Pinch ti sento. Direi che lo strumento di confronto profondo in unitilsè difettoso proprio perché confronta le variabili anche quando potrebbero non avere un impatto osservabile . Un'altra conseguenza (indesiderabile) del confronto delle variabili è che le chiusure pure (senza uno stato proprio) non sono supportate. Inoltre, richiede che gli oggetti confrontati siano dello stesso tipo di runtime. Mi sono rimboccato le maniche e ho creato la mia versione dello strumento di confronto profondo che affronta queste preoccupazioni.
beluchin

@ Wolfgang c'è qualche codice di esempio a cui indirizzarci? Da dove hai preso quella citazione?
anon58192932

30

Amo questa domanda! Soprattutto perché non viene quasi mai risposto o risposto male. È come se nessuno l'avesse ancora capito. Territorio vergine :)

Prima di tutto, non pensare nemmeno all'utilizzo equals. Il contratto di equals, come definito nel javadoc, è una relazione di equivalenza (riflessiva, simmetrica e transitiva), non una relazione di uguaglianza. Per questo, dovrebbe anche essere antisimmetrico. L'unica implementazione di equalsciò è (o potrebbe mai essere) una vera relazione di uguaglianza è quella in java.lang.Object. Anche se hai usato equalsper confrontare tutto nel grafico, il rischio di rompere il contratto è piuttosto alto. Come ha sottolineato Josh Bloch in Effective Java , il contratto di uguaglianza è molto facile da rompere:

"Semplicemente non c'è modo di estendere una classe istanziabile e aggiungere un aspetto preservando il contratto uguale"

Inoltre, a cosa serve davvero un metodo booleano? Sarebbe bello incapsulare effettivamente tutte le differenze tra l'originale e il clone, non credi? Inoltre, presumo qui che tu non voglia essere disturbato dalla scrittura / manutenzione del codice di confronto per ogni oggetto nel grafico, ma piuttosto stai cercando qualcosa che si ridimensionerà con la fonte mentre cambia nel tempo.

Quindi, quello che vuoi veramente è una sorta di strumento di confronto statale. Il modo in cui questo strumento viene implementato dipende in realtà dalla natura del modello di dominio e dalle limitazioni delle prestazioni. Nella mia esperienza, non esiste una pallottola magica generica. E sarà lento per un gran numero di iterazioni. Ma per testare la completezza di un'operazione di clonazione, farà abbastanza bene il lavoro. Le tue due migliori opzioni sono la serializzazione e la riflessione.

Alcuni problemi che incontrerai:

  • Ordine di raccolta: due raccolte devono essere considerate simili se contengono gli stessi oggetti, ma in un ordine diverso?
  • Quali campi ignorare: transitori? Statico?
  • Equivalenza del tipo: i valori dei campi devono essere esattamente dello stesso tipo? O va bene che uno estenda l'altro?
  • C'è di più, ma dimentico ...

XStream è abbastanza veloce e combinato con XMLUnit farà il lavoro in poche righe di codice. XMLUnit è carino perché può segnalare tutte le differenze o semplicemente fermarsi alla prima che trova. E il suo output include l'xpath ai diversi nodi, il che è carino. Per impostazione predefinita, non consente raccolte non ordinate, ma può essere configurato per farlo. L'iniezione di un gestore delle differenze speciale (chiamato a DifferenceListener) consente di specificare il modo in cui si desidera gestire le differenze, incluso l'ignorare l'ordine. Tuttavia, non appena si desidera fare qualcosa oltre la più semplice personalizzazione, diventa difficile scrivere ei dettagli tendono ad essere legati a un oggetto di dominio specifico.

La mia preferenza personale è quella di utilizzare la riflessione per scorrere tutti i campi dichiarati e approfondire ciascuno di essi, monitorando le differenze man mano che procedo. Parola di avvertimento: non utilizzare la ricorsione a meno che non ti piacciano le eccezioni di overflow dello stack. Mantieni le cose nell'ambito con uno stack (usa un fileLinkedListo qualcosa). Di solito ignoro i campi transitori e statici e salto le coppie di oggetti che ho già confrontato, quindi non finisco in cicli infiniti se qualcuno decide di scrivere codice autoreferenziale (Tuttavia, confronto sempre i wrapper primitivi, non importa cosa , poiché gli stessi riferimenti oggetto vengono spesso riutilizzati). Puoi configurare le cose in anticipo per ignorare l'ordinamento della raccolta e per ignorare tipi o campi speciali, ma mi piace definire le mie politiche di confronto dello stato sui campi stessi tramite annotazioni. Questo, IMHO, è esattamente ciò a cui erano destinate le annotazioni, per rendere disponibili i metadati sulla classe in fase di esecuzione. Qualcosa di simile a:


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

Penso che questo sia in realtà un problema davvero difficile, ma totalmente risolvibile! E una volta che hai qualcosa che funziona per te, è davvero, davvero, utile :)

Quindi buona fortuna. E se ti viene in mente qualcosa di puro genio, non dimenticare di condividerlo!


15

Vedi DeepEquals e DeepHashCode () all'interno di java-util: https://github.com/jdereg/java-util

Questa classe fa esattamente ciò che l'autore originale richiede.


4
Avviso: DeepEquals utilizza il metodo .equals () di un oggetto, se esistente. Questo potrebbe non essere quello che vuoi.
Adam

4
Utilizza solo .equals () su una classe se è stato aggiunto esplicitamente un metodo equals (), altrimenti esegue un confronto membro per membro. La logica qui è che se qualcuno si è sforzato di scrivere un metodo equals () personalizzato, allora dovrebbe essere usato. Miglioramento futuro: consenti a flag di ignorare i metodi equals () anche se esistono. Ci sono utilità utili in java-util, come CaseInsensitiveMap / Set.
John DeRegnaucourt

Mi preoccupo di confrontare i campi. La differenza nei campi potrebbe non essere osservabile dal punto di vista del client degli oggetti e comunque un confronto approfondito basato sui campi la contrassegnerebbe. Inoltre, il confronto dei campi richiede che gli oggetti siano dello stesso tipo di runtime, il che potrebbe essere limitante.
beluchin

Per rispondere a @beluchin sopra, DeepEquals.deepEquals () non fa sempre un confronto campo per campo. In primo luogo, ha un'opzione per usare .equals () su un metodo se ne esiste uno (che non è quello su Object), oppure può essere ignorato. In secondo luogo, quando si confrontano mappe / raccolte, non prende in considerazione il tipo di raccolta o mappa, né i campi della raccolta / mappa. Invece, li confronta logicamente. Una LinkedHashMap può essere uguale a una TreeMap se hanno gli stessi contenuti ed elementi nello stesso ordine. Per le raccolte e le mappe non ordinate, sono richiesti solo articoli di dimensioni e di uguale profondità.
John DeRegnaucourt

quando si confrontano Mappe / Raccolte, non prende in considerazione il tipo di Raccolta o Mappa, né i campi della Collezione / Mappa. Invece, li confronta logicamente @JohnDeRegnaucourt, vorrei sostenere questo confronto logico, ovvero confrontare solo ciò che publicdovrebbe essere applicato a tutti i tipi invece di essere applicabile solo a raccolta / mappe.
beluchin

10

Sostituisci il metodo equals ()

Puoi semplicemente sovrascrivere il metodo equals () della classe utilizzando EqualsBuilder.reflectionEquals () come spiegato qui :

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

7

Dovevo solo implementare il confronto di due istanze di entità riviste da Hibernate Envers. Ho iniziato a scrivere il mio differire ma poi ho trovato il seguente framework.

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

Puoi confrontare due oggetti dello stesso tipo e mostrerà modifiche, aggiunte e rimozioni. Se non ci sono modifiche, gli oggetti sono uguali (in teoria). Vengono fornite annotazioni per i getter che dovrebbero essere ignorati durante il controllo. Il frame work ha applicazioni molto più ampie del controllo di uguaglianza, ovvero che sto usando per generare un registro delle modifiche.

Le sue prestazioni sono OK, quando si confrontano le entità JPA, assicurarsi di scollegarle prima dal gestore entità.


6

Sto usando 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
Raccolte diverse dagli elenchi possono restituire elementi in un ordine diverso, quindi il confronto tra stringhe avrà esito negativo.
Alexey Berezkin

Anche le classi che non sono serializzabili falliranno
Zwelch

6

In AssertJ , puoi fare:

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

Probabilmente non funzionerà in tutti i casi, tuttavia funzionerà in più casi che potresti pensare.

Ecco cosa dice la documentazione:

Asserire che l'oggetto in prova (effettivo) è uguale all'oggetto dato in base al confronto ricorsivo di una proprietà / campo per proprietà / campo (compresi quelli ereditati). Questo può essere utile se l'implementazione uguale a actual non ti soddisfa. Il confronto ricorsivo proprietà / campo non viene applicato ai campi che hanno un'implementazione di uguale personalizzato, ovvero verrà utilizzato il metodo uguale a sovrascritto invece di un campo per confronto di campo.

Il confronto ricorsivo gestisce i cicli. Per impostazione predefinita, i float vengono confrontati con una precisione di 1.0E-6 e raddoppia con 1.0E-15.

È possibile specificare un comparatore personalizzato per i campi (nidificati) o digitare rispettivamente usingComparatorForFields (Comparator, String ...) e usingComparatorForType (Comparator, Class).

Gli oggetti da confrontare possono essere di diversi tipi ma devono avere le stesse proprietà / campi. Ad esempio, se l'oggetto effettivo ha un nome String field, è previsto che anche l'altro oggetto ne abbia uno. Se un oggetto ha un campo e una proprietà con lo stesso nome, il valore della proprietà verrà utilizzato sul campo.


1
isEqualToComparingFieldByFieldRecursivelyè ora deprecato. Usa assertThat(expectedObject).usingRecursiveComparison().isEqualTo(actualObject);invece :)
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
particolarmente utile se devi gestire le classi generate, dove non hai alcuna influenza sugli uguali!
Matthias B

1
stackoverflow.com/a/1449051/829755 lo ha già menzionato. avresti dovuto modificare quel post
user829755

1
@ user829755 In questo modo perdo punti. Quindi tutto sul gioco a punti)) Alla gente piace ricevere crediti per il lavoro svolto, lo sono anch'io.
gavenkoa

3

Hamcrest ha lo stesso MatcherPropertyValuesAs . Ma si basa sulla Convenzione JavaBeans (utilizza getter e setter). Se gli oggetti che devono essere confrontati non hanno getter e setter per i loro attributi, questo non funzionerà.

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

Il bean utente - con getter e setter

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

}

Funziona alla grande finché non hai un POJO che utilizza un isFoometodo di lettura per una Booleanproprietà. C'è un PR aperto dal 2016 per risolverlo. github.com/hamcrest/JavaHamcrest/pull/136
Snekse

2

Se i tuoi oggetti implementano Serializable puoi usare questo:

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

Il tuo esempio di elenco collegato non è così difficile da gestire. Quando il codice attraversa i due grafici degli oggetti, posiziona gli oggetti visitati in un Set o in una Mappa. Prima di passare a un altro riferimento a un oggetto, questo set viene testato per vedere se l'oggetto è già stato attraversato. Se è così, non c'è bisogno di andare oltre.

Sono d'accordo con la persona sopra che ha detto di usare un LinkedList (come uno Stack ma senza metodi sincronizzati su di esso, quindi è più veloce). Attraversare l'oggetto grafico utilizzando una pila, mentre si utilizza la riflessione per ottenere ogni campo, è la soluzione ideale. Scritto una volta, questo "esterno" equals () e "esterno" hashCode () è ciò che tutti i metodi equals () e hashCode () dovrebbero chiamare. Non avrai mai più bisogno di un metodo cliente uguale ().

Ho scritto un po 'di codice che attraversa un grafico a oggetti completo, elencato su Google Code. Vedi json-io (http://code.google.com/p/json-io/). Serializza un oggetto grafico Java in JSON e deserializzato da esso. Gestisce tutti gli oggetti Java, con o senza costruttori pubblici, Serializzabile o non Serializzabile, ecc. Questo stesso codice di attraversamento sarà la base per l'implementazione "equals ()" ed esterna "hashcode ()". A proposito, JsonReader / JsonWriter (json-io) è generalmente più veloce di ObjectInputStream / ObjectOutputStream incorporato.

Questo JsonReader / JsonWriter potrebbe essere utilizzato per il confronto, ma non aiuterà con il codice hash. Se vuoi un hashcode universale () e uguale (), ha bisogno del proprio codice. Potrei riuscire a farcela con un visitatore generico del grafico. Vedremo.

Altre considerazioni - campi statici - è facile - possono essere ignorati perché tutte le istanze equals () avrebbero lo stesso valore per i campi statici, poiché i campi statici sono condivisi tra tutte le istanze.

Per quanto riguarda i campi transitori, questa sarà un'opzione selezionabile. A volte potresti volere che i transienti contino altre volte no. "A volte ti senti un pazzo, a volte no."

Torna al progetto json-io (per i miei altri progetti) e troverai il progetto esterno equals () / hashcode (). Non ho ancora un nome, ma sarà ovvio.


1

Apache ti dà qualcosa, converti entrambi gli oggetti in stringa e confronta le stringhe, ma devi sovrascrivere aString ()

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

Sostituisci toString ()

Se tutti i campi sono tipi primitivi:

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

Se hai campi e / o collezione e / o mappa non primitivi:

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

Immagino che tu lo sappia, ma in teoria dovresti sempre sovrascrivere .equals per affermare che due oggetti sono veramente uguali. Ciò implicherebbe che controllino i metodi .equals sostituiti sui loro membri.

Questo genere di cose è il motivo per cui .equals è definito in Object.

Se ciò fosse fatto in modo coerente, non avresti problemi.


2
Il problema è che voglio automatizzare il test per una base di codice esistente di grandi dimensioni che non ho scritto ... :)
Uri

0

Una garanzia ferma per un confronto così profondo potrebbe essere un problema. Cosa dovrebbe fare quanto segue? (Se si implementa un simile comparatore, questo sarebbe un buon test unitario.)

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

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

Eccone un altro:

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

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

Se hai una nuova domanda, chiedila facendo clic sul pulsante Fai domanda . Includere un collegamento a questa domanda se aiuta a fornire il contesto.
YoungHobbit

@younghobbit: no questa non è una nuova domanda. Un punto interrogativo in una risposta non rende quella bandiera appropriata. Si prega di prestare maggiore attenzione.
Ben Voigt

Da questo: Using an answer instead of a comment to get a longer limit and better formatting.se questo è un commento, perché usare la sezione delle risposte? Ecco perché l'ho segnalato. non a causa del ?. Questa risposta è già stata segnalata da qualcun altro, che non ha lasciato il commento. Ho appena ricevuto questo nella coda delle revisioni. Potrebbe essere il mio problema, avrei dovuto essere più attento.
YoungHobbit

0

Penso che la soluzione più semplice ispirata alla soluzione di Ray Hulha sia serializzare l'oggetto e quindi confrontare in profondità il risultato grezzo.

La serializzazione può essere byte, json, xml o semplice toString ecc. ToString sembra essere più economico. Lombok genera per noi ToSTring personalizzabili e gratuiti. Vedi esempio sotto.

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.