¿Hay una utilidad de reflexión de Java para hacer una comparación profunda de dos objetos?

Intento escribir pruebas unitarias para una variedad de operaciones de clone() dentro de un proyecto grande y me pregunto si hay una clase existente en alguna parte que sea capaz de tomar dos objetos del mismo tipo, haciendo una comparación profunda, y diciendo si son idénticos o no?

Unitils tiene esta funcionalidad:

Aserción de igualdad a través de la reflexión, con diferentes opciones como ignorar los valores predeterminados / nulos de Java e ignorar el orden de las colecciones

¡Me encanta esta pregunta! Principalmente porque casi nunca se responde o responde mal. Es como si nadie lo hubiera descubierto todavía. Territorio virgen 🙂

En primer lugar, ni siquiera pienses en usar equals . El contrato de equals , como se define en el javadoc, es una relación de equivalencia (reflexiva, simétrica y transitiva), no una relación de igualdad. Para eso, también tendría que ser antisimétrico. La única implementación de equals que es (o podría ser) una verdadera relación de igualdad es la de java.lang.Object . Incluso si usó equals para comparar todo en el gráfico, el riesgo de romper el contrato es bastante alto. Como señaló Josh Bloch en Effective Java , el contrato de iguales es muy fácil de romper:

“Simplemente no hay forma de extender una clase instanciable y agregar un aspecto mientras se preserva el contrato igual”

Además, ¿de qué te sirve realmente un método booleano? Sería bueno encapsular todas las diferencias entre el original y el clon, ¿no crees? Además, asumiré aquí que no desea que le moleste escribir / mantener el código de comparación para cada objeto en el gráfico, sino que está buscando algo que se escale con la fuente a medida que cambia con el tiempo.

Así que, lo que realmente quieres es algún tipo de herramienta de comparación de estados. Cómo se implementa esa herramienta depende realmente de la naturaleza de su modelo de dominio y sus restricciones de rendimiento. En mi experiencia, no hay una bala mágica genérica. Y será lento en una gran cantidad de iteraciones. Pero para probar la integridad de una operación de clonación, hará el trabajo bastante bien. Sus dos mejores opciones son la serialización y la reflexión.

Algunos problemas que encontrará:

  • Orden de recostackción: ¿Deberían considerarse similares dos colecciones si contienen los mismos objetos, pero en un orden diferente?
  • ¿Qué campos ignorar: transitorio? ¿Estático?
  • Tipo de equivalencia: ¿Los valores de campo deben ser exactamente del mismo tipo? ¿O está bien que uno amplíe el otro?
  • Hay más, pero me olvido …

XStream es bastante rápido y combinado con XMLUnit hará el trabajo en unas pocas líneas de código. XMLUnit es bueno porque puede informar todas las diferencias, o simplemente detenerse en el primero que encuentre. Y su salida incluye el xpath a los diferentes nodos, lo cual es bueno. Por defecto, no permite colecciones desordenadas, pero se puede configurar para hacerlo. Inyectar un manejador de diferencia especial (llamado DifferenceListener ) le permite especificar la forma en que desea lidiar con las diferencias, incluso ignorar el orden. Sin embargo, tan pronto como desee hacer algo más allá de la personalización más simple, se vuelve difícil escribir y los detalles tienden a estar vinculados a un objeto de dominio específico.

Mi preferencia personal es usar la reflexión para recorrer todos los campos declarados y profundizar en cada uno, rastreando las diferencias a medida que avanzo. Palabra de advertencia: No use la recursividad a menos que le gusten las excepciones de desbordamiento de stack. Mantenga las cosas en el scope con una stack (use una LinkedList o algo así). Normalmente ignoro campos transitorios y estáticos, y omito pares de objetos que ya he comparado, así que no termino en bucles infinitos si alguien decide escribir código autorreferencial (Sin embargo, siempre comparo envoltorios primitivos sin importar qué , ya que los mismos refs de objeto a menudo se reutilizan). Puede configurar las cosas desde el principio para ignorar el orden de la colección e ignorar los tipos o campos especiales, pero me gusta definir mis políticas de comparación de estado en los campos a través de anotaciones. Esto, en mi humilde opinión, es exactamente para lo que se escribieron las anotaciones, para hacer metadatos sobre la clase disponible en tiempo de ejecución. Algo como:

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

Creo que este es un problema realmente difícil, ¡pero totalmente solucionable! Y una vez que tienes algo que funciona para ti, es realmente, realmente, útil 🙂

Buena suerte. Y si se te ocurre algo genial, ¡no te olvides de compartir!

Consulte DeepEquals y DeepHashCode () dentro de java-util: https://github.com/jdereg/java-util

Esta clase hace exactamente lo que el autor original solicita.

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

Solo tuve que implementar la comparación de dos instancias de entidades revisadas por Hibernate Envers. Empecé a escribir mi propia diferencia, pero luego encontré el siguiente marco.

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

Puede comparar dos objetos del mismo tipo y mostrará los cambios, las adiciones y las eliminaciones. Si no hay cambios, entonces los objetos son iguales (en teoría). Las anotaciones se proporcionan para captadores que deben ignorarse durante el control. El trabajo de marco tiene aplicaciones mucho más amplias que la comprobación de igualdad, es decir, estoy usando para generar un registro de cambios.

Su rendimiento está bien, al comparar entidades JPA, asegúrese de separarlas primero del administrador de entidades.

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

Si tus objetos implementan Serializable, puedes usar esto:

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

Anular el método equals ()

Simplemente puede anular el método equals () de la clase usando EqualsBuilder.reflectionEquals () como se explica aquí :

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

Su ejemplo de lista enlazada no es tan difícil de manejar. A medida que el código atraviesa los dos gráficos de objetos, coloca los objetos visitados en un conjunto o mapa. Antes de pasar a otra referencia de objeto, este conjunto se prueba para ver si el objeto ya ha sido atravesado. Si es así, no hay necesidad de ir más allá.

Estoy de acuerdo con la persona de arriba que dijo usar una LinkedList (como una stack pero sin métodos sincronizados, por lo que es más rápido). Atravesar el gráfico de objetos usando una stack, mientras usa la reflexión para obtener cada campo, es la solución ideal. Escrito una vez, este “externo” es igual a () y “externo” hashCode () es lo que deben llamar todos los métodos equals () y hashCode (). Nunca más necesitas un método de cliente igual a ().

Escribí un poco de código que atraviesa un gráfico completo de objetos, que figura en Google Code. Consulte json-io (http://code.google.com/p/json-io/). Serializa un gráfico de objetos Java en JSON y se deserializa a partir de él. Maneja todos los objetos Java, con o sin constructores públicos, serializables o no serializables, etc. Este mismo código transversal será la base de la implementación externa “igual ()” y externa “código hash ()”. Por cierto, el JsonReader / JsonWriter (json-io) suele ser más rápido que el ObjectInputStream / ObjectOutputStream incorporado.

Este JsonReader / JsonWriter podría usarse para comparar, pero no ayudará con hashcode. Si quiere un hashcode () universal y es igual a (), necesita su propio código. Puedo ser capaz de llevarlo a cabo con un visitante gráfico genérico. Ya veremos.

Otras consideraciones – campos estáticos – eso es fácil – se pueden omitir porque todas las instancias equals () tendrían el mismo valor para los campos estáticos, ya que los campos estáticos se comparten en todas las instancias.

En cuanto a los campos transitorios, esa será una opción seleccionable. A veces puede querer que los transitorios cuenten otras veces que no. “A veces te sientes como un loco, a veces no”.

Vuelva al proyecto json-io (para mis otros proyectos) y encontrará el proyecto externo equals () / hashcode (). Aún no tengo un nombre, pero será obvio.

Apache te da algo, convierte ambos objetos en una cadena y compara cadenas, pero tienes que sobrescribir toString ()

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

Anular toString ()

Si todos los campos son tipos primitivos:

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

Si tiene campos no primitivos y / o colección y / o mapa:

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

En AssertJ , puedes hacer:

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

Probablemente no funcionará en todos los casos, sin embargo, funcionará en más casos de lo que piensas.

Esto es lo que dice la documentación:

Afirme que el objeto bajo prueba (real) es igual al objeto dado en función de una propiedad / campo recursivo por comparación de propiedad / campo (incluidos los heredados). Esto puede ser útil si la implementación igual de real no le conviene. La comparación de propiedad / campo recursiva no se aplica en los campos que tienen una implementación de igual personalizado, es decir, se usará el método de igualdad anulada en lugar de una comparación de campo por campo.

La comparación recursiva maneja ciclos. Por defecto, los flotantes se comparan con una precisión de 1.0E-6 y dobles con 1.0E-15.

Puede especificar un comparador personalizado por campos (nesteds) o escribir con respectivamente usandoComparatorForFields (Comparator, String …) y usingComparatorForType (Comparator, Class).

Los objetos para comparar pueden ser de diferentes tipos pero deben tener las mismas propiedades / campos. Por ejemplo, si el objeto real tiene un nombre Campo de cadena, se espera que el otro objeto también tenga uno. Si un objeto tiene un campo y una propiedad con el mismo nombre, el valor de la propiedad se usará sobre el campo.

Supongo que sabes esto, pero en teoría, se supone que siempre debes anular a los iguales para afirmar que dos objetos son verdaderamente iguales. Esto implicaría que controlan los métodos .equals anulados en sus miembros.

Este tipo de cosas es por qué .equals se define en Object.

Si esto se hiciera de manera consistente, no tendrías ningún problema.

Hamcrest tiene el Matcher samePropertyValuesAs . Pero depende de la Convención JavaBeans (utiliza getters y setters). Si los objetos que se van a comparar no tienen getters y setters para sus atributos, esto no funcionará.

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

El usuario Bean – con getters y setters

 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 garantía de suspensión para una comparación tan profunda podría ser un problema. ¿Qué debe hacer lo siguiente? (Si implementa dicho comparador, esto sería una buena prueba de unidad).

 LinkedListNode a = new LinkedListNode(); a.next = a; LinkedListNode b = new LinkedListNode(); b.next = b; System.out.println(DeepCompare(a, b)); 

Aquí está otro:

 LinkedListNode c = new LinkedListNode(); LinkedListNode d = new LinkedListNode(); c.next = d; d.next = c; System.out.println(DeepCompare(c, d)); 

Creo que la solución más sencilla inspirada en la solución Ray Hulha es serializar el objeto y luego comparar en profundidad el resultado sin procesar.

La serialización podría ser byte, json, xml o simple toString, etc. ToString parece ser más barato. Lombok genera ToSTring personalizable fácil y gratuito para nosotros. Vea el ejemplo a continuación.

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