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


99

Estoy tratando de escribir pruebas unitarias para una variedad de clone()operaciones dentro de un proyecto grande y me pregunto si existe una clase en algún lugar que sea capaz de tomar dos objetos del mismo tipo, hacer una comparación profunda y decir si ¿Eres idéntico o no?


1
¿Cómo sabría esta clase si en un punto determinado de los gráficos de objetos puede aceptar objetos idénticos o solo las mismas referencias?
Zed

Idealmente, será lo suficientemente configurable :) Estoy buscando algo automático para que si se agregan nuevos campos (y no se clonan), la prueba pueda identificarlos.
Uri

3
Lo que estoy tratando de decir es que necesitará configurar (es decir, implementar) las comparaciones de todos modos. Entonces, ¿por qué no anular el método equals en sus clases y usarlo?
Zed

3
Si es igual a devuelve falso para un objeto complejo grande, ¿por dónde empezar? Es mucho mejor convertir el objeto en una cadena de varias líneas y hacer una comparación de cadenas. Entonces puede ver exactamente dónde son diferentes dos objetos. IntelliJ muestra una ventana de comparación de "cambios" que ayuda a encontrar múltiples cambios entre dos resultados, es decir, comprende la salida de assertEquals (cadena1, cadena2) y le brinda una ventana de comparación.
Peter Lawrey

Hay algunas respuestas realmente buenas aquí, además de la aceptada, que parecen haber sido enterradas
user1445967

Respuestas:


62

Unitils tiene esta funcionalidad:

Afirmació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


9
He hecho algunas pruebas en esta función y parece hacer una comparación profunda donde EqualsBuilder no lo hace.
Howard May

¿Hay alguna manera de que esto no ignore los campos transitorios?
Pellizque

@Pinch te escucho. Yo diría que la herramienta de comparación profunda unitilstiene fallas precisamente porque compara variables incluso cuando pueden no tener un impacto observable . Otra consecuencia (indeseable) de comparar variables es que los cierres puros (sin estado propio) no son compatibles. Además, requiere que los objetos comparados sean del mismo tipo de tiempo de ejecución. Me arremangué y creé mi propia versión de la herramienta de comparación profunda que aborda estas preocupaciones.
beluchin

@Wolfgang, ¿hay algún código de muestra al que dirigirnos? ¿De dónde sacaste esa cita?
anon58192932

30

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

En primer lugar, ni siquiera pienses en usarlo 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 equalseso es (o podría ser) una verdadera relación de igualdad es la que se encuentra en java.lang.Object. Incluso si solía equalscomparar 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 de igualdad"

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 quiere preocuparse por escribir / mantener el código de comparación para cada objeto en el gráfico, sino que está buscando algo que se adapte a la fuente a medida que cambia con el tiempo.

Entonces, lo que realmente quieres es algún tipo de herramienta de comparación de estados. La forma en que se implementa esa herramienta depende realmente de la naturaleza de su modelo de dominio y sus restricciones de rendimiento. En mi experiencia, no existe una fórmula 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, funcionará bastante bien. Sus dos mejores opciones son la serialización y la reflexión.

Algunos problemas que encontrará:

  • Orden de colección: ¿Dos colecciones deben considerarse similares si contienen los mismos objetos, pero en un orden diferente?
  • ¿Qué campos ignorar: transitorio? ¿Estático?
  • Equivalencia de tipos: ¿Deberían los valores de campo ser exactamente del mismo tipo? ¿O está bien que uno extienda el otro?
  • Hay más, pero me olvido ...

XStream es bastante rápido y combinado con XMLUnit hará el trabajo en solo unas pocas líneas de código. XMLUnit es bueno porque puede informar todas las diferencias, o simplemente detenerse en la primera que encuentre. Y su salida incluye el xpath a los diferentes nodos, lo cual es bueno. De forma predeterminada, no permite colecciones desordenadas, pero se puede configurar para que lo haga. La inyección de un controlador de diferencias especial (denominado a DifferenceListener) le permite especificar la forma en que desea tratar las diferencias, incluido el ignorar el orden. Sin embargo, tan pronto como quiera hacer algo más allá de la personalización más simple, se vuelve difícil escribir y los detalles tienden a estar ligados 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. Advertencia: no utilice la recursividad a menos que le gusten las excepciones de desbordamiento de pila. Mantenga las cosas dentro del alcance con una pila (use unLinkedListo algo). Por lo general, ignoro los campos estáticos y transitorios, y omito pares de objetos que ya he comparado, por lo que no termino en bucles infinitos si alguien decide escribir código autorreferencial (sin embargo, siempre comparo envoltorios primitivos sin importar qué , ya que las mismas referencias de objeto a menudo se reutilizan). Puede configurar las cosas por adelantado para ignorar el orden de la colección y para ignorar tipos o campos especiales, pero me gusta definir mis políticas de comparación de estado en los campos mismos a través de anotaciones. Esto, en mi humilde opinión, es exactamente para lo que estaban destinadas las anotaciones, para hacer que los metadatos sobre la clase estén disponibles en tiempo de ejecución. Algo como:


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

Creo que este es un problema realmente difícil, ¡pero totalmente solucionable! Y una vez que tenga algo que funcione para usted, es realmente muy útil :)

Buena suerte. Y si se te ocurre algo que es pura genialidad, ¡no olvides compartirlo!


15

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

Esta clase hace exactamente lo que solicita el autor original.


4
Advertencia: DeepEquals usa el método .equals () de un objeto, si existe. Puede que esto no sea lo que quieres.
Adam

4
Solo usa .equals () en una clase si se agregó explícitamente un método equals (); de lo contrario, hace una comparación miembro por miembro. La lógica aquí es que si alguien hizo el esfuerzo de escribir un método equals () personalizado, entonces debería usarse. Mejora futura: permite que flag ignore los métodos equals () incluso si existen. Hay utilidades útiles en java-util, como CaseInsensitiveMap / Set.
John DeRegnaucourt

Me preocupo por comparar campos. La diferencia en los campos puede no ser observable desde el punto de vista del cliente de los objetos y aún así una comparación profunda basada en los campos la marcaría. Además, comparar campos requiere que los objetos sean del mismo tipo de tiempo de ejecución, lo que puede ser limitante.
beluchin

Para responder a @beluchin arriba, DeepEquals.deepEquals () no siempre hace una comparación campo por campo. Primero, tiene una opción para usar .equals () en un método si existe uno (que no es el de Object), o puede ignorarse. En segundo lugar, al comparar mapas / colecciones, no se fija en el tipo de colección o mapa, ni en los campos de la colección / mapa. En cambio, los compara lógicamente. Un LinkedHashMap puede igualar a un TreeMap si tienen los mismos contenidos y elementos en el mismo orden. Para colecciones y mapas no ordenados, solo se requieren elementos de tamaño y profundidad igual.
John DeRegnaucourt

al comparar Mapas / Colecciones, no mira el Tipo de Colección o Mapa, ni los campos en la Colección / Mapa. En cambio, los compara lógicamente @JohnDeRegnaucourt, yo diría que esta comparación lógica, es decir, comparar solo lo que publicdebería aplicarse a todos los tipos en lugar de solo ser aplicable a colecciones / mapas.
beluchin

10

Anular el método equals ()

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

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

7

Solo tuve que implementar la comparación de dos instancias de entidad revisadas por Hibernate Envers. Comencé 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á cambios, adiciones y eliminaciones. Si no hay cambios, entonces los objetos son iguales (en teoría). Se proporcionan anotaciones para los captadores que deben ignorarse durante la verificación. El marco de trabajo tiene aplicaciones mucho más amplias que la verificación de igualdad, es decir, estoy usando para generar un registro de cambios.

Su rendimiento es correcto, al comparar entidades JPA, asegúrese de separarlas del administrador de la entidad primero.


6

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

5
Las colecciones distintas de las listas pueden devolver elementos en un orden diferente, por lo que la comparación de cadenas fallará.
Alexey Berezkin

También las clases que no son serializables fallarán
Zwelch

6

En AssertJ , puede hacer:

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

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

Esto es lo que dice la documentación:

Afirmar que el objeto bajo prueba (real) es igual al objeto dado basado en una comparación recursiva de propiedad / campo por propiedad / campo (incluidos los heredados). Esto puede ser útil si la implementación igual a real no le conviene. La comparación recursiva de propiedad / campo no se aplica en campos que tienen una implementación igual a personalizada, es decir, se utilizará el método igual anulado en lugar de una comparación campo por campo.

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

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

Los objetos a comparar pueden ser de diferentes tipos pero deben tener las mismas propiedades / campos. Por ejemplo, si el objeto real tiene un campo de cadena de nombre, 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 utilizará sobre el campo.


1
isEqualToComparingFieldByFieldRecursivelyahora está en desuso. Úselo en su assertThat(expectedObject).usingRecursiveComparison().isEqualTo(actualObject);lugar :)
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
especialmente útil si tienes que manejar clases generadas, donde no tienes ninguna influencia sobre iguales.
Matthias B

1
stackoverflow.com/a/1449051/829755 ya mencionó esto. debería haber editado esa publicación
user829755

1
@ user829755 De esta manera pierdo puntos. SO todo sobre el juego de puntos)) A la gente le gusta obtener créditos por el trabajo hecho, a mí también.
gavenkoa

3

Hamcrest tiene el Matcher samePropertyValuesAs . Pero se basa en la Convención de JavaBeans (usa 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 bean de usuario - 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;
    }

}

Esto funciona muy bien hasta que tenga un POJO que esté usando un isFoométodo de lectura para una Booleanpropiedad. Hay un PR que ha estado abierto desde 2016 para solucionarlo. github.com/hamcrest/JavaHamcrest/pull/136
Snekse

2

Si sus objetos implementan Serializable, puede 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);
    }
}

1

Su ejemplo de Linked List no es tan difícil de manejar. A medida que el código atraviesa los gráficos de dos objetos, coloca los objetos visitados en un Conjunto o Mapa. Antes de atravesar otra referencia de objeto, este conjunto se prueba para ver si el objeto ya ha sido atravesado. Si es así, no es necesario ir más lejos.

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

Escribí un fragmento de código que atraviesa un gráfico de objetos completo, incluido en Google Code. Consulte json-io (http://code.google.com/p/json-io/). Serializa un gráfico de objetos Java en JSON y lo deserializa. Maneja todos los objetos Java, con o sin constructores públicos, serializables o no serializables, etc. Este mismo código transversal será la base para la implementación externa "equals ()" y externa "hashcode ()". Por cierto, JsonReader / JsonWriter (json-io) suele ser más rápido que el ObjectInputStream / ObjectOutputStream integrado.

Este JsonReader / JsonWriter podría usarse para comparar, pero no ayudará con el código hash. Si desea un código hash universal () y equals (), necesita su propio código. Es posible que pueda lograr esto con un visitante de 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 entre todas las instancias.

En cuanto a los campos transitorios, esa será una opción seleccionable. A veces es posible que desee que los transitorios cuenten otras veces 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 (). Todavía no tengo un nombre, pero será obvio.


1

Apache le da algo, convierta ambos objetos en cadenas y compare cadenas, pero debe anular 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--;
        }
    }
}}

0

Supongo que lo sabe, pero en teoría, se supone que siempre debe anular .equals para afirmar que dos objetos son realmente iguales. Esto implicaría que verifican los métodos .equals reemplazados en sus miembros.

Este tipo de cosas es la razón por la que .equals se define en Object.

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


2
El problema es que quiero automatizar la prueba de esto para una gran base de código existente que no escribí ... :)
Uri

0

Una garantía vacilante para una comparación tan profunda podría ser un problema. ¿Qué debe hacer lo siguiente? (Si implementa un comparador de este tipo, sería una buena prueba unitaria).

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

Si tiene una nueva pregunta, hágala haciendo clic en el botón Preguntar . Incluya un enlace a esta pregunta si ayuda a proporcionar contexto.
YoungHobbit

@younghobbit: no, esta no es una pregunta nueva. Un signo de interrogación en una respuesta no hace que esa bandera sea apropiada. Por favor, preste más atención.
Ben Voigt

De esto: Using an answer instead of a comment to get a longer limit and better formatting.Si esto es un comentario, entonces, ¿por qué usar la sección de respuestas? Por eso lo marqué. no por el ?. Esta respuesta ya está marcada por otra persona, que no dejó el comentario atrás. Acabo de recibir esto en la cola de revisión. Podría ser mi culpa, debería haber tenido más cuidado.
YoungHobbit

0

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

La serialización puede ser byte, json, xml o simple toString, etc. ToString parece ser más económico. Lombok genera ToSTring personalizable 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());
    }
}   

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.