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

Sto provando a scrivere test unitari per una varietà di operazioni clone() all’interno di un grande progetto e mi chiedo se esiste una class esistente da qualche parte in grado di prendere due oggetti dello stesso tipo, facendo un confronto profondo, e dicendo se sono identici o no?

Unitils ha questa funzionalità:

Asserzione di uguaglianza attraverso la riflessione, con diverse opzioni come ignorare i valori di default / null di Java e ignorare l’ordine delle collezioni

Adoro questa domanda! Soprattutto perché difficilmente ha mai risposto o risposto male. È come se nessuno l’avesse ancora capito. Territorio vergine 🙂

Prima di tutto, non pensare nemmeno all’utilizzo di 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 equals che è (o potrebbe mai essere) una vera relazione di uguaglianza è quella di java.lang.Object . Anche se hai usato equals per confrontare tutto nel grafico, il rischio di rompere il contratto è piuttosto alto. Come Josh Bloch ha sottolineato in Java efficace , il contratto di pari è molto facile da rompere:

“Semplicemente non c’è modo di estendere una class istantanea e aggiungere un aspetto preservando il contratto di uguaglianza”

Inoltre, a che serve davvero un metodo booleano? Sarebbe bello incapsulare davvero tutte le differenze tra l’originale e il clone, non credi? Inoltre, assumerò qui che non vuoi essere preoccupato di scrivere / mantenere il codice di confronto per ogni object nel grafico, ma piuttosto stai cercando qualcosa che si ridimensiona con la sorgente mentre cambia nel tempo.

Soooo, quello che vuoi veramente è una specie di strumento di confronto tra stati. Il modo in cui lo strumento viene implementato dipende in realtà dalla natura del modello di dominio e dalle limitazioni delle prestazioni. Nella mia esperienza, non esiste un proiettile magico generico. E sarà lento su 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: le due raccolte devono essere considerate simili se contengono gli stessi oggetti, ma in un ordine diverso?
  • Quali campi da ignorare: transitorio? Statico?
  • Equivalenza di tipo: i valori dei campi dovrebbero essere esattamente dello stesso tipo? O è giusto per uno estendere l’altro?
  • C’è di più, ma dimentico …

XStream è piuttosto veloce e combinato con XMLUnit farà il lavoro in poche righe di codice. XMLUnit è bello perché può riportare tutte le differenze, o semplicemente fermarsi al primo che trova. E il suo output include il percorso x per i diversi nodes, che è bello. Per impostazione predefinita, non consente raccolte non ordinate, ma può essere configurato per farlo. L’inserimento di un gestore di differenze speciali (Chiamato a DifferenceListener ) consente di specificare il modo in cui si desidera gestire le differenze, compreso l’ignorare l’ordine. Tuttavia, non appena si vuole fare qualcosa oltre la semplice personalizzazione, diventa difficile scrivere e i dettagli tendono ad essere legati ad un object dominio specifico.

La mia preferenza personale è usare la riflessione per scorrere tutti i campi dichiarati e approfondire ognuno di essi, tracciando le differenze mentre procedo. Parola di avviso: non utilizzare la ricorsione a meno che non ti piacciano le eccezioni di overflow dello stack. Mantieni le cose nello scope con uno stack (usa una LinkedList o qualcosa del genere). Di solito ignoro i campi transitori e statici, e salta le coppie di oggetti che ho già confrontato, quindi non finisco in loop infiniti se qualcuno ha deciso di scrivere un codice autoreferenziale (Tuttavia, ho sempre confrontato i wrapper primitivi, non importa cosa , dal momento che gli stessi oggetti ref vengono spesso riutilizzati). È ansible configurare le cose in anticipo per ignorare l’ordinamento delle raccolte e ignorare i tipi o i campi speciali, ma mi piace definire le politiche di confronto degli stati sui campi stessi tramite annotazioni. Questo, IMHO, è esattamente ciò a cui le annotazioni erano destinate, per rendere i metadati sulla class disponibili al runtime. Qualcosa di simile a:

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

Penso che questo sia davvero un problema davvero difficile, ma totalmente risolvibile! E una volta che hai qualcosa che funziona per te, è davvero, davvero, a portata di mano 🙂

Perciò buona fortuna. E se ti viene in mente qualcosa di puro genio, non dimenticare di condividere!

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

Questa class fa esattamente ciò che l’autore originale richiede.

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

Bastava implementare il confronto di due istanze di quadro riviste da Hibernate Envers. Ho iniziato a scrivere le mie differenze, ma poi ho trovato il seguente schema.

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

È ansible confrontare due oggetti dello stesso tipo e mostrerà modifiche, aggiunte e rimozioni. Se non ci sono cambiamenti, allora gli oggetti sono uguali (in teoria). Le annotazioni sono fornite per i getter che dovrebbero essere ignorati durante il controllo. Il frame work ha applicazioni molto più ampie rispetto al controllo di uguaglianza, cioè sto usando per generare un log di modifiche.

Le sue prestazioni sono OK, quando si confrontano le entity framework JPA, assicurarsi di rimuoverle prima dal gestore quadro.

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

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

Sostituisci il metodo equals ()

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

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

L’esempio dell’elenco collegato non è così difficile da gestire. Mentre il codice attraversa i due grafici dell’object, colloca gli oggetti visitati in un Set o in una Mappa. Prima di attraversare un altro riferimento a un object, questo set viene testato per verificare se l’object è già stato attraversato. Se è così, non c’è bisogno di andare oltre.

Sono d’accordo con la persona sopra che ha detto di usare una LinkedList (come una pila ma senza metodi sincronizzati su di essa, quindi è più veloce). Attraversare il grafico dell’object usando una pila, mentre si usa la riflessione per ottenere ogni campo, è la soluzione ideale. Scritto una volta, questo “esterno” equals () e “external” hashCode () è ciò che tutti i metodi equals () e hashCode () dovrebbero chiamare. Mai più hai bisogno di un metodo customer equals ().

Ho scritto un po ‘di codice che attraversa un grafico completo degli oggetti, elencato su Google Code. Vedi json-io (http://code.google.com/p/json-io/). Serializza un grafo di oggetti Java in JSON e deserializza da esso. Gestisce tutti gli oggetti Java, con o senza costruttori pubblici, Serializzabile o non serializzabile, ecc. Questo stesso codice trasversale sarà la base per l’implementazione esterna “equals ()” ed esterna “hashcode ()”. Btw, JsonReader / JsonWriter (json-io) è in genere più veloce di ObjectInputStream / ObjectOutputStream incorporato.

Questo JsonReader / JsonWriter potrebbe essere utilizzato per il confronto, ma non sarà d’aiuto con hashcode. Se vuoi un hashcode () e un equals () universali, ha bisogno del suo codice. Potrei riuscire a farlo con un visitatore grafico generico. Vedremo.

Altre considerazioni – campi statici – è facile – possono essere saltati perché tutte le istanze di equals () avranno lo stesso valore per i campi statici, poiché i campi statici sono condivisi tra tutte le istanze.

Per quanto riguarda i campi transienti, questa sarà un’opzione selezionabile. A volte potresti volere che i transienti contino altre volte no. “A volte ti senti un pazzo, a volte non lo fai.”

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

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

 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 non primitivi e / o collezione e / o mappa:

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

In AssertJ , puoi fare:

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

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

Ecco cosa dice la documentazione:

Asserire che l’object in prova (effettivo) è uguale all’object dato basato su una proprietà / campo ricorsivo per confronto di proprietà / campo (compresi quelli ereditati). Questo può essere utile se l’effettiva implementazione di pari non è adatta a te. Il confronto di proprietà / campo ricorsivo non viene applicato ai campi con un’implementazione personalizzata uguale, vale a dire verrà utilizzato il metodo di override uguale anziché un confronto campo per campo.

Il confronto ricorsivo gestisce i cicli. Di default i float vengono confrontati con una precisione di 1.0E-6 e raddoppia con 1.0E-15.

È ansible specificare un comparatore personalizzato per campi (nidificati) o digitare rispettivamente usandoComparatorForFields (Comparator, String …) e usingComparatorForType (Comparator, Class).

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

Immagino che tu lo sappia, ma in teoria, si suppone che tu debba sempre prevalere. Equivale ad affermare che due oggetti sono veramente uguali. Ciò implicherebbe che controllino i metodi .equals sovrascritti sui loro membri.

Questo genere di cose è perché .equals è definito in Object.

Se ciò fosse fatto in modo coerente non avresti un problema.

Hamcrest ha il Matcher samePropertyValuesAs . Ma si basa sulla convenzione JavaBeans (usa 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; } } 

Una ferma garanzia per un confronto così profondo potrebbe essere un problema. Cosa dovrebbe fare il seguente? (Se si implementa un tale 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)); 

Penso che la soluzione più semplice ispirata alla soluzione di Ray Hulha sia serializzare l’object 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 gratuitamente ToSTring personalizzabile gratuito per noi. Vedi l’esempio qui 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()); } }