Existe um utilitário de reflexão Java para fazer uma comparação profunda de dois objetos?


99

Estou tentando escrever testes de unidade para uma variedade de clone()operações dentro de um grande projeto e me pergunto se existe uma classe em algum lugar que seja capaz de pegar dois objetos do mesmo tipo, fazer uma comparação profunda e dizer se eles é idêntico ou não?


1
Como essa classe saberia se em um determinado ponto dos gráficos de objetos ela pode aceitar objetos idênticos ou apenas as mesmas referências?
Zed

O ideal é que seja configurável o suficiente :) Estou procurando algo automático para que se novos campos forem adicionados (e não clonados) o teste possa identificá-los.
Uri

3
O que estou tentando dizer é que você precisará configurar (ou seja, implementar) as comparações de qualquer maneira. Então, por que não substituir o método equals em suas classes e usá-lo?
Zed

3
Se igual a retorna falso para um objeto grande e complexo, por onde você começa? É muito melhor transformar o objeto em uma String de várias linhas e fazer uma comparação de String. Então você pode ver exatamente onde dois objetos são diferentes. O IntelliJ exibe uma janela de comparação de "alterações" que ajuda a encontrar várias alterações entre dois resultados, ou seja, ele entende a saída de assertEquals (string1, string2) e fornece uma janela de comparação.
Peter Lawrey

Existem algumas respostas muito boas aqui, além da aceita, que parecem ter sido enterradas
user1445967

Respostas:


62

Unitils tem esta funcionalidade:

Asserção de igualdade por meio de reflexão, com diferentes opções, como ignorar valores Java padrão / nulos e ignorar a ordem das coleções


9
Eu fiz alguns testes nesta função e parece fazer uma comparação profunda, enquanto EqualsBuilder não faz.
Howard maio,

Existe uma maneira de fazer com que isso não ignore os campos transitórios?
Beliscar

@Pinch estou ouvindo você. Eu diria que a ferramenta de comparação profunda em unitilsé falha precisamente porque compara variáveis ​​mesmo quando elas podem não ter um impacto observável . Outra consequência (indesejável) da comparação de variáveis ​​é que encerramentos puros (sem estado próprio) não são suportados. Além disso, requer que os objetos comparados sejam do mesmo tipo de tempo de execução. Arregacei as mangas e criei minha própria versão da ferramenta de comparação profunda que trata dessas questões.
beluchin

@Wolfgang existe algum código de amostra para nos indicar? De onde você tirou essa citação?
anon58192932

30

Eu amo essa pergunta! Principalmente porque quase nunca é respondida ou mal respondida. É como se ninguém ainda tivesse descoberto. Território virgem :)

Primeiro, nem pense em usar equals. O contrato deequals , conforme definido no javadoc, é uma relação de equivalência (reflexiva, simétrica e transitiva), não uma relação de igualdade. Para isso, também teria que ser anti-simétrico. A única implementação equalsdisso é (ou poderia ser) uma verdadeira relação de igualdade em java.lang.Object. Mesmo que você use equalspara comparar tudo no gráfico, o risco de quebra do contrato é bem alto. Como Josh Bloch apontou em Effective Java , o contrato de iguais é muito fácil de quebrar:

"Simplesmente não há maneira de estender uma classe instanciável e adicionar um aspecto preservando o contrato de igual"

Além do mais, de que serve um método booleano? Seria bom encapsular todas as diferenças entre o original e o clone, você não acha? Além disso, assumirei aqui que você não quer se incomodar em escrever / manter código de comparação para cada objeto no gráfico, mas em vez disso, está procurando por algo que será escalado com a origem à medida que ela muda com o tempo.

Entããão, o que você realmente quer é algum tipo de ferramenta de comparação de estado. A forma como essa ferramenta é implementada realmente depende da natureza de seu modelo de domínio e de suas restrições de desempenho. Na minha experiência, não existe uma fórmula mágica genérica. E será lento em um grande número de iterações. Mas para testar a integridade de uma operação de clone, ele fará o trabalho muito bem. Suas duas melhores opções são serialização e reflexão.

Alguns problemas que você encontrará:

  • Ordem da coleção: duas coleções devem ser consideradas semelhantes se contiverem os mesmos objetos, mas em uma ordem diferente?
  • Quais campos devem ser ignorados: Transitório? Estático?
  • Equivalência de tipo: os valores dos campos devem ser exatamente do mesmo tipo? Ou será que um pode estender o outro?
  • Tem mais, mas eu esqueço ...

XStream é muito rápido e combinado com XMLUnit fará o trabalho em apenas algumas linhas de código. XMLUnit é bom porque pode relatar todas as diferenças, ou apenas parar na primeira que encontrar. E sua saída inclui o xpath para os nós diferentes, o que é bom. Por padrão, ele não permite coleções não ordenadas, mas pode ser configurado para isso. Injetando um manipulador de diferença especial (chamado deDifferenceListener ) permite que você especifique a maneira como deseja lidar com as diferenças, incluindo ignorar a ordem. No entanto, assim que você deseja fazer algo além da personalização mais simples, torna-se difícil escrever e os detalhes tendem a ser vinculados a um objeto de domínio específico.

Minha preferência pessoal é usar reflexão para percorrer todos os campos declarados e detalhar cada um, rastreando as diferenças conforme eu prossigo. Aviso: não use recursão, a menos que goste de exceções de estouro de pilha. Mantenha as coisas dentro do escopo com uma pilha (use umLinkedListou alguma coisa). Eu geralmente ignoro os campos transientes e estáticos, e pulo os pares de objetos que já comparei, então não acabo em loops infinitos se alguém decidir escrever código auto-referencial (No entanto, eu sempre comparo wrappers primitivos, não importa o que , uma vez que os mesmos refs de objeto são frequentemente reutilizados). Você pode configurar as coisas com antecedência para ignorar a ordem da coleção e para ignorar tipos ou campos especiais, mas eu gosto de definir minhas políticas de comparação de estado nos próprios campos por meio de anotações. Isso, IMHO, é exatamente para o que as anotações foram feitas, para disponibilizar metadados sobre a classe em tempo de execução. Algo como:


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

Acho que esse é um problema realmente difícil, mas totalmente solucionável! E quando você tem algo que funciona para você, é muito, muito útil :)

Então boa sorte. E se você vier com algo que é pura genialidade, não se esqueça de compartilhar!


15

Consulte DeepEquals e DeepHashCode () em java-util: https://github.com/jdereg/java-util

Esta classe faz exatamente o que o autor original solicita.


4
Aviso: DeepEquals usa o método .equals () de um objeto, se houver. Isso pode não ser o que você deseja.
Adam

4
Ele só usa .equals () em uma classe se um método equals () foi explicitamente adicionado, caso contrário, ele faz uma comparação membro por membro. A lógica aqui é que se alguém se esforçou para escrever um método equals () personalizado, ele deve ser usado. Aprimoramento futuro: permite que o sinalizador ignore os métodos equals () mesmo se eles existirem. Existem utilitários úteis em java-util, como CaseInsensitiveMap / Set.
John DeRegnaucourt,

Eu me preocupo em comparar campos. A diferença nos campos pode não ser observável do ponto de vista do cliente dos objetos e ainda assim uma comparação profunda com base nos campos a sinalizaria. Além disso, a comparação de campos requer que os objetos sejam do mesmo tipo de tempo de execução, o que pode ser limitante.
beluchin

Para responder @beluchin acima, DeepEquals.deepEquals () nem sempre faz uma comparação campo a campo. Primeiro, ele tem a opção de usar .equals () em um método, se houver (que não seja o de Object), ou pode ser ignorado. Em segundo lugar, ao comparar Mapas / Coleções, ele não olha para a Coleção ou tipo de Mapa, nem para os campos da Coleção / Mapa. Em vez disso, ele os compara logicamente. Um LinkedHashMap pode ser igual a um TreeMap se eles tiverem o mesmo conteúdo e elementos na mesma ordem. Para coleções e mapas não ordenados, apenas itens de tamanho e profundidade iguais são necessários.
John DeRegnaucourt

ao comparar Mapas / Coleções, não observa o tipo de Coleção ou Mapa, nem os campos da Coleção / Mapa. Em vez disso, ele os compara logicamente @JohnDeRegnaucourt, eu argumentaria essa comparação lógica, ou seja, comparando apenas o que publicdeve se aplicar a todos os tipos, ao invés de ser aplicável apenas a coleções / mapas.
beluchin

10

Substituir o método equals ()

Você pode simplesmente substituir o método equals () da classe usando EqualsBuilder.reflectionEquals () conforme explicado aqui :

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

7

Apenas tive que implementar a comparação de duas instâncias de entidade revisadas por Hibernate Envers. Comecei a escrever meu próprio diferente, mas depois encontrei a seguinte estrutura.

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

Você pode comparar dois objetos do mesmo tipo e ele mostrará alterações, adições e remoções. Se não houver mudanças, então os objetos são iguais (em teoria). As anotações são fornecidas para getters que devem ser ignorados durante a verificação. O quadro de trabalho tem aplicações muito mais amplas do que verificação de igualdade, ou seja, estou usando para gerar um log de alterações.

Seu desempenho é OK, ao comparar entidades JPA, certifique-se de separá-las do gerenciador de entidades primeiro.


6

Estou usando o 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
Coleções diferentes de listas podem retornar elementos em ordem diferente, portanto, a comparação de strings falhará.
Alexey Berezkin,

Além disso, as classes que não são serializáveis ​​falharão
Zwelch

6

Em AssertJ , você pode fazer:

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

Provavelmente não funcionará em todos os casos, mas funcionará em mais casos do que você imagina.

Aqui está o que a documentação diz:

Afirme que o objeto em teste (real) é igual ao objeto fornecido com base em uma propriedade / campo recursiva por comparação de propriedade / campo (incluindo os herdados). Isso pode ser útil se a implementação real de igual a você não for adequada. A comparação recursiva de propriedade / campo não é aplicada em campos com uma implementação personalizada igual, ou seja, o método igual a substituído será usado em vez de um campo por comparação de campo.

A comparação recursiva lida com ciclos. Por padrão, os flutuadores são comparados com uma precisão de 1.0E-6 e dobra com 1.0E-15.

Você pode especificar um comparador personalizado por campos (aninhados) ou tipo com, respectivamente, usingComparatorForFields (Comparator, String ...) e usingComparatorForType (Comparator, Class).

Os objetos a comparar podem ser de tipos diferentes, mas devem ter as mesmas propriedades / campos. Por exemplo, se o objeto real tiver um campo String de nome, espera-se que o outro objeto também tenha um. Se um objeto tiver um campo e uma propriedade com o mesmo nome, o valor da propriedade será usado sobre o campo.


1
isEqualToComparingFieldByFieldRecursivelyagora está obsoleto. Use em assertThat(expectedObject).usingRecursiveComparison().isEqualTo(actualObject);vez :)
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 se você tiver que lidar com classes geradas, onde você não tem nenhuma influência sobre iguais!
Matthias B

1
stackoverflow.com/a/1449051/829755 já mencionou isso. você deveria ter editado essa postagem
user829755

1
@ user829755 Desta forma, perco pontos. TÃO tudo sobre jogo de pontos)) As pessoas gostam de receber créditos pelo trabalho realizado, eu também.
gavenkoa

3

Hamcrest tem o Matcher samePropertyValuesAs . Mas ele se baseia na Convenção JavaBeans (usa getters e setters). Se os objetos a serem comparados não tiverem getters e setters para seus atributos, isso não 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
    }
}

O bean do usuário - com getters e 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;
    }

}

Isso funciona muito bem até que você tenha um POJO que esteja usando um isFoométodo de leitura para uma Booleanpropriedade. Há um PR que está aberto desde 2016 para consertar isso. github.com/hamcrest/JavaHamcrest/pull/136
Snekse

2

Se seus objetos implementam Serializable, você pode usar isto:

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

Seu exemplo de lista vinculada não é tão difícil de manusear. Conforme o código atravessa os dois gráficos de objeto, ele coloca os objetos visitados em um Conjunto ou Mapa. Antes de atravessar para outra referência de objeto, este conjunto é testado para ver se o objeto já foi atravessado. Em caso afirmativo, não há necessidade de ir mais longe.

Concordo com a pessoa acima que disse usar um LinkedList (como um Stack, mas sem métodos sincronizados nele, então é mais rápido). Percorrer o gráfico do objeto usando uma Pilha, enquanto usa reflexão para obter cada campo, é a solução ideal. Escrito uma vez, este hashCode () "externo" equals () e "externo" é o que todos os métodos equals () e hashCode () devem chamar. Nunca mais você precisará de um método equals () do cliente.

Eu escrevi um pedaço de código que atravessa um gráfico de objeto completo, listado no Google Code. Consulte json-io (http://code.google.com/p/json-io/). Ele serializa um gráfico de objeto Java em JSON e desserializa a partir dele. Ele lida com todos os objetos Java, com ou sem construtores públicos, Serializáveis ​​ou não Serializáveis, etc. Este mesmo código de passagem será a base para a implementação externa "equals ()" e externa "hashcode ()". A propósito, o JsonReader / JsonWriter (json-io) é geralmente mais rápido do que o ObjectInputStream / ObjectOutputStream integrado.

Este JsonReader / JsonWriter poderia ser usado para comparação, mas não ajudará com o hashcode. Se você deseja um hashcode universal () e equals (), ele precisa de seu próprio código. Posso conseguir fazer isso com um visitante de gráfico genérico. Veremos.

Outras considerações - campos estáticos - que são fáceis - podem ser ignoradas porque todas as instâncias de equals () teriam o mesmo valor para campos estáticos, já que os campos estáticos são compartilhados por todas as instâncias.

Quanto aos campos transitórios - essa será uma opção selecionável. Às vezes você pode querer que os transientes sejam contados, outras vezes não. "Às vezes você se sente um maluco, às vezes não."

Volte para o projeto json-io (para meus outros projetos) e você encontrará o projeto externo equals () / hashcode (). Ainda não tenho um nome para isso, mas será óbvio.


1

O Apache dá a você algo, converta ambos os objetos em string e compare strings, mas você tem que Substituir toString ()

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

Substituir toString ()

Se todos os campos forem tipos primitivos:

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

Se você tiver campos e / ou coleção e / ou mapa não primitivos:

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

Eu acho que você sabe disso, mas em teoria, você deve sempre substituir .equals para afirmar que dois objetos são realmente iguais. Isso implicaria que eles verificassem os métodos .equals substituídos em seus membros.

É por esse tipo de coisa que .equals é definido em Object.

Se isso fosse feito de forma consistente, você não teria um problema.


2
O problema é que eu quero automatizar o teste para uma grande base de código existente que eu não escrevi ... :)
Uri

0

Uma garantia provisória para uma comparação tão profunda pode ser um problema. O que o seguinte deve fazer? (Se você implementar tal comparador, isso seria um bom teste de unidade.)

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

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

Aqui está outro:

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

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

Se você tiver uma nova pergunta, faça-a clicando no botão Fazer pergunta . Inclua um link para esta pergunta se ajudar a fornecer contexto.
YoungHobbit

@younghobbit: não, esta não é uma questão nova. Um ponto de interrogação em uma resposta não torna esse sinalizador apropriado. Por favor, preste mais atenção.
Ben Voigt,

Disto: Using an answer instead of a comment to get a longer limit and better formatting.Se este é um comentário, por que usar a seção de resposta? É por isso que sinalizei. não por causa do ?. Esta resposta já foi sinalizada por outra pessoa, que não deixou o comentário. Acabei de colocar isso na fila de revisão. Pode ser meu mal, eu deveria ter sido mais cuidadoso.
YoungHobbit de

0

Acho que a solução mais fácil inspirada na solução de Ray Hulha é serializar o objeto e depois comparar profundamente o resultado bruto.

A serialização pode ser byte, json, xml ou toString simples, etc. ToString parece ser mais barato. O Lombok gera ToSTring customizável fácil e gratuito para nós. Veja o exemplo abaixo.

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

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.