Existe-t-il un utilitaire de réflexion Java pour effectuer une comparaison approfondie de deux objets?


99

J'essaie d'écrire des tests unitaires pour une variété d' clone()opérations dans un grand projet et je me demande s'il existe une classe existante quelque part capable de prendre deux objets du même type, de faire une comparaison approfondie et de dire s'ils sont identiques ou pas?


1
Comment cette classe pourrait-elle savoir si à un certain point des graphes d'objets elle peut accepter des objets identiques, ou seulement les mêmes références?
Zed

Idéalement, il sera suffisamment configurable :) Je recherche quelque chose d'automatique pour que si de nouveaux champs sont ajoutés (et non clonés), le test puisse les identifier.
Uri

3
Ce que j'essaie de dire, c'est que vous devrez de toute façon configurer (c'est-à-dire implémenter) les comparaisons. Alors pourquoi ne pas remplacer la méthode equals dans vos classes et l'utiliser?
Zed le

3
Si égal à renvoie false pour un grand objet complexe, par où commencer? Vous feriez bien mieux de transformer l'objet en une chaîne multiligne et de faire une comparaison de chaînes. Ensuite, vous pouvez voir exactement où deux objets sont différents. IntelliJ affiche une fenêtre de comparaison «changements» qui aide à trouver plusieurs changements entre deux résultats, c'est-à-dire qu'il comprend la sortie de assertEquals (chaîne1, chaîne2) et vous donne une fenêtre de comparaison.
Peter Lawrey

Il y a de très bonnes réponses ici, en plus de celle acceptée, qui semblent avoir été enterrées
user1445967

Réponses:


63

Unitils a cette fonctionnalité:

Assertion d'égalité par réflexion, avec différentes options comme ignorer les valeurs par défaut / nul de Java et ignorer l'ordre des collections


9
J'ai fait quelques tests sur cette fonction et il semble faire une comparaison approfondie là où EqualsBuilder ne le fait pas.
Howard mai

Existe-t-il un moyen de ne pas ignorer les champs transitoires?
Pincer le

@Pinch je vous entends. Je dirais que l'outil de comparaison approfondie unitilsest défectueux précisément parce qu'il compare des variables même lorsqu'elles peuvent ne pas avoir d' impact observable . Une autre conséquence (indésirable) de la comparaison des variables est que les fermetures pures (sans état propre) ne sont pas prises en charge. De plus, les objets comparés doivent être du même type d'exécution. J'ai retroussé mes manches et créé ma propre version de l'outil de comparaison approfondie qui répond à ces préoccupations.
beluchin le

@Wolfgang existe-t-il un exemple de code vers lequel nous diriger? D'où avez-vous tiré cette citation?
anon58192932

30

J'adore cette question! Principalement parce qu'il est rarement répondu ou mal répondu. C'est comme si personne ne l'avait encore compris. Territoire vierge :)

Tout d'abord, ne pensez même pas à utiliser equals. Le contrat de equals, tel que défini dans le javadoc, est une relation d'équivalence (réflexive, symétrique et transitive), pas une relation d'égalité. Pour cela, il faudrait aussi qu'il soit antisymétrique. La seule implémentation de equalscela est (ou pourrait être) une vraie relation d'égalité est celle dans java.lang.Object. Même si vous avez utilisé equalspour comparer tout dans le graphique, le risque de rupture du contrat est assez élevé. Comme l'a souligné Josh Bloch dans Effective Java , le contrat d'égal à égal est très facile à rompre:

"Il n'y a tout simplement aucun moyen d'étendre une classe instanciable et d'ajouter un aspect tout en préservant le contrat égal"

En plus à quoi sert une méthode booléenne de toute façon? Ce serait bien d'encapsuler réellement toutes les différences entre l'original et le clone, vous ne pensez pas? De plus, je suppose ici que vous ne voulez pas être dérangé par l'écriture / la maintenance du code de comparaison pour chaque objet du graphique, mais plutôt que vous recherchez quelque chose qui s'adapte à la source à mesure qu'elle change au fil du temps.

Soooo, ce que vous voulez vraiment, c'est une sorte d'outil de comparaison d'état. La manière dont cet outil est mis en œuvre dépend vraiment de la nature de votre modèle de domaine et de vos restrictions de performances. D'après mon expérience, il n'y a pas de solution magique générique. Et il sera lent sur un grand nombre d'itérations. Mais pour tester l'exhaustivité d'une opération de clonage, cela fera assez bien le travail. Vos deux meilleures options sont la sérialisation et la réflexion.

Quelques problèmes que vous rencontrerez:

  • Ordre des collections: deux collections doivent-elles être considérées comme similaires si elles contiennent les mêmes objets, mais dans un ordre différent?
  • Quels champs ignorer: transitoire? Statique?
  • Équivalence de type: les valeurs de champ doivent-elles être exactement du même type? Ou est-ce que l'un peut prolonger l'autre?
  • Il y a plus, mais j'oublie ...

XStream est assez rapide et combiné avec XMLUnit fera le travail en seulement quelques lignes de code. XMLUnit est agréable car il peut signaler toutes les différences, ou simplement s'arrêter à la première qu'il trouve. Et sa sortie inclut le xpath vers les différents nœuds, ce qui est bien. Par défaut, il n'autorise pas les collections non ordonnées, mais il peut être configuré pour le faire. Injection d'un gestionnaire de différence spécial (appelé unDifferenceListener ) vous permet de spécifier la façon dont vous souhaitez traiter les différences, y compris en ignorant l'ordre. Cependant, dès que vous voulez faire quelque chose au-delà de la personnalisation la plus simple, cela devient difficile à écrire et les détails ont tendance à être liés à un objet de domaine spécifique.

Ma préférence personnelle est d'utiliser la réflexion pour parcourir tous les champs déclarés et explorer chacun d'eux, en suivant les différences au fur et à mesure. Mot d'avertissement: n'utilisez pas la récursivité sauf si vous aimez les exceptions de dépassement de capacité de pile. Gardez les choses dans la portée avec une pile (utilisez unLinkedListou quelque chose). J'ignore généralement les champs transitoires et statiques, et je saute les paires d'objets que j'ai déjà comparées, donc je ne me retrouve pas dans des boucles infinies si quelqu'un décide d'écrire du code auto-référentiel (Cependant, je compare toujours les wrappers primitifs quoi qu'il arrive , puisque les mêmes références d'objet sont souvent réutilisées). Vous pouvez configurer les choses à l'avance pour ignorer l'ordre des collections et pour ignorer les types ou les champs spéciaux, mais j'aime définir mes politiques de comparaison d'état sur les champs eux-mêmes via des annotations. Ceci, à mon humble avis, est exactement ce à quoi les annotations étaient destinées, pour rendre les métadonnées sur la classe disponibles au moment de l'exécution. Quelque chose comme:


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

Je pense que c'est en fait un problème vraiment difficile, mais totalement résoluble! Et une fois que vous avez quelque chose qui fonctionne pour vous, c'est vraiment, vraiment, pratique :)

Alors bonne chance. Et si vous trouvez quelque chose de pur génie, n'oubliez pas de partager!


15

Voir DeepEquals et DeepHashCode () dans java-util: https://github.com/jdereg/java-util

Cette classe fait exactement ce que l'auteur original demande.


4
Attention: DeepEquals utilise la méthode .equals () d'un objet s'il en existe une. Ce n'est peut-être pas ce que vous voulez.
Adam

4
Il n'utilise .equals () sur une classe que si une méthode equals () a été explicitement ajoutée, sinon il effectue une comparaison membre par membre. La logique ici est que si quelqu'un s'est efforcé d'écrire une méthode equals () personnalisée, elle devrait être utilisée. Amélioration future: autorisez flag à ignorer les méthodes equals () même si elles existent. Il existe des utilitaires utiles dans java-util, comme CaseInsensitiveMap / Set.
John DeRegnaucourt

Je m'inquiète de comparer les champs. La différence dans les champs peut ne pas être observable du point de vue du client des objets et une comparaison approfondie basée sur les champs la marquerait. De plus, la comparaison des champs nécessite que les objets soient du même type d'exécution, ce qui peut être limitatif.
beluchin

Pour répondre à @beluchin ci-dessus, DeepEquals.deepEquals () ne fait pas toujours une comparaison champ par champ. Premièrement, il a une option pour utiliser .equals () sur une méthode s'il en existe une (qui n'est pas celle sur Object), ou il peut être ignoré. Deuxièmement, lors de la comparaison des cartes / collections, il ne regarde pas le type de collection ou de carte, ni les champs de la collection / carte. Au lieu de cela, il les compare logiquement. Un LinkedHashMap peut égaler un TreeMap s'ils ont le même contenu et les mêmes éléments dans le même ordre. Pour les collections et les cartes non commandées, seuls les éléments de taille et de profondeur égale sont requis.
John DeRegnaucourt

lors de la comparaison des cartes / collections, il ne regarde pas le type de collection ou de carte, ni les champs de la collection / carte. Au lieu de cela, il les compare logiquement @JohnDeRegnaucourt, je soutiendrais cette comparaison logique, c'est-à-dire en comparant uniquement ce qui publicdevrait s'appliquer à tous les types au lieu de s'appliquer uniquement à la collection / aux cartes.
beluchin

10

Remplacer la méthode equals ()

Vous pouvez simplement remplacer la méthode equals () de la classe en utilisant EqualsBuilder.reflectionEquals () comme expliqué ici :

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

7

Juste eu à implémenter la comparaison de deux instances d'entités révisées par Hibernate Envers. J'ai commencé à écrire mes propres différences, mais j'ai ensuite trouvé le cadre suivant.

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

Vous pouvez comparer deux objets du même type et il affichera les modifications, les ajouts et les suppressions. S'il n'y a pas de changement, les objets sont égaux (en théorie). Des annotations sont fournies pour les getters qui doivent être ignorés lors de la vérification. Le cadre a des applications beaucoup plus larges que la vérification d'égalité, c'est-à-dire que j'utilise pour générer un journal des modifications.

Ses performances sont correctes, lorsque vous comparez des entités JPA, assurez-vous de les détacher d'abord du gestionnaire d'entités.


6

J'utilise 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
Les collections autres que les listes peuvent renvoyer des éléments dans un ordre différent, donc la comparaison de chaînes échouera.
Alexey Berezkin

Aussi les classes qui ne sont pas sérialisables échoueront
Zwelch

6

Dans AssertJ , vous pouvez faire:

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

Cela ne fonctionnera probablement pas dans tous les cas, mais cela fonctionnera dans plus de cas que vous pensez.

Voici ce que dit la documentation:

Affirmez que l'objet testé (réel) est égal à l'objet donné en fonction d'une comparaison récursive d'une propriété / champ par propriété / champ (y compris ceux hérités). Cela peut être utile si l'implémentation égale de réel ne vous convient pas. La comparaison propriété / champ récursive n'est pas appliquée sur les champs ayant une implémentation d'égalité personnalisée, c'est-à-dire que la méthode d'égalité remplacée sera utilisée à la place d'un champ par comparaison de champ.

La comparaison récursive gère les cycles. Par défaut, les flottants sont comparés avec une précision de 1.0E-6 et doublent avec 1.0E-15.

Vous pouvez spécifier un comparateur personnalisé par champs (imbriqués) ou par type avec respectivement usingComparatorForFields (Comparator, String ...) et usingComparatorForType (Comparator, Class).

Les objets à comparer peuvent être de types différents mais doivent avoir les mêmes propriétés / champs. Par exemple, si l'objet réel a un champ de chaîne de nom, il est attendu que l'autre objet en ait également un. Si un objet a un champ et une propriété avec le même nom, la valeur de la propriété sera utilisée sur le champ.


1
isEqualToComparingFieldByFieldRecursivelyest désormais obsolète. Utilisez assertThat(expectedObject).usingRecursiveComparison().isEqualTo(actualObject);plutôt :)
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
particulièrement utile si vous devez gérer des classes générées, où vous n'avez aucune influence sur les égaux!
Matthias B

1
stackoverflow.com/a/1449051/829755 l'a déjà mentionné. vous devriez avoir édité ce message
user829755

1
@ user829755 De cette façon, je perds des points. Donc tout sur le jeu de points)) Les gens aiment obtenir des crédits pour le travail accompli, moi aussi.
gavenkoa

3

Hamcrest a le Matcher samePropertyValuesAs . Mais il repose sur la convention JavaBeans (utilise des getters et des setters). Si les objets à comparer n'ont pas de getters et de setters pour leurs attributs, cela ne fonctionnera pas.

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

Le bean utilisateur - avec les getters et les 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;
    }

}

Cela fonctionne très bien jusqu'à ce que vous ayez un POJO qui utilise une isFoométhode de lecture pour une Booleanpropriété. Il y a un PR ouvert depuis 2016 pour y remédier. github.com/hamcrest/JavaHamcrest/pull/136
Snekse

2

Si vos objets implémentent Serializable, vous pouvez utiliser ceci:

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

Votre exemple de liste liée n'est pas si difficile à gérer. Au fur et à mesure que le code parcourt les deux graphiques d'objets, il place les objets visités dans un ensemble ou une carte. Avant de passer dans une autre référence d'objet, cet ensemble est testé pour voir si l'objet a déjà été parcouru. Si oui, inutile d'aller plus loin.

Je suis d'accord avec la personne ci-dessus qui a dit utiliser une LinkedList (comme une pile mais sans méthodes synchronisées dessus, donc c'est plus rapide). Traverser le graphe d'objets à l'aide d'une pile, tout en utilisant la réflexion pour obtenir chaque champ, est la solution idéale. Écrit une fois, ce hashCode () "externe" equals () et "externe" est ce que toutes les méthodes equals () et hashCode () devraient appeler. Vous n'avez plus jamais besoin d'une méthode customer equals ().

J'ai écrit un peu de code qui traverse un graphique d'objets complet, répertorié sur Google Code. Voir json-io (http://code.google.com/p/json-io/). Il sérialise un graphe d'objets Java en JSON et en désérialise. Il gère tous les objets Java, avec ou sans constructeurs publics, sérialisables ou non sérialisables, etc. Ce même code de traversée sera la base de l'implémentation externe "equals ()" et externe "hashcode ()". Btw, le JsonReader / JsonWriter (json-io) est généralement plus rapide que le ObjectInputStream / ObjectOutputStream intégré.

Ce JsonReader / JsonWriter pourrait être utilisé à des fins de comparaison, mais cela n'aidera pas avec le hashcode. Si vous voulez un hashcode universel () et equals (), il a besoin de son propre code. Je pourrais peut-être réussir cela avec un visiteur graphique générique. Nous verrons.

Autres considérations - les champs statiques - c'est facile - ils peuvent être ignorés car toutes les instances equals () auraient la même valeur pour les champs statiques, car les champs statiques sont partagés entre toutes les instances.

Quant aux champs transitoires, ce sera une option sélectionnable. Parfois, vous voudrez peut-être que les transitoires comptent d'autres fois pas. "Parfois tu te sens comme un cinglé, parfois non."

Revenez au projet json-io (pour mes autres projets) et vous trouverez le projet externe equals () / hashcode (). Je n'ai pas encore de nom pour cela, mais ce sera évident.


1

Apache vous donne quelque chose, convertissez les deux objets en chaîne et comparez les chaînes, mais vous devez remplacer toString ()

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

Remplacer toString ()

Si tous les champs sont de types primitifs:

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

Si vous avez des champs non primitifs et / ou une collection et / ou une carte:

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

Je suppose que vous le savez, mais en théorie, vous êtes censé toujours remplacer .equals pour affirmer que deux objets sont vraiment égaux. Cela impliquerait qu'ils vérifient les méthodes .equals surchargées sur leurs membres.

Ce genre de chose est la raison pour laquelle .equals est défini dans Object.

Si cela était fait de manière cohérente, vous n'auriez pas de problème.


2
Le problème est que je veux automatiser le test pour une grande base de code existante que je n'ai pas écrite ... :)
Uri

0

Une garantie d'arrêt pour une comparaison aussi approfondie pourrait être un problème. Que doivent faire les éléments suivants? (Si vous implémentez un tel comparateur, cela ferait un bon test unitaire.)

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

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

En voici une autre:

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

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

Si vous avez une nouvelle question, posez-la en cliquant sur le bouton Poser une question . Incluez un lien vers cette question si cela permet de fournir un contexte.
YoungHobbit

@younghobbit: non ce n'est pas une nouvelle question. Un point d'interrogation dans une réponse ne rend pas cet indicateur approprié. Veuillez prêter plus d'attention.
Ben Voigt

De ceci: Using an answer instead of a comment to get a longer limit and better formatting.S'il s'agit d'un commentaire, pourquoi utiliser la section réponse? C'est pourquoi je l'ai signalé. pas à cause du ?. Cette réponse est déjà signalée par quelqu'un d'autre, qui n'a pas laissé le commentaire derrière. Je viens de recevoir cela dans la file d'attente des critiques. Ça pourrait être mon mal, j'aurais dû être plus prudent.
YoungHobbit

0

Je pense que la solution la plus simple inspirée de la solution Ray Hulha est de sérialiser l'objet, puis de comparer en profondeur le résultat brut.

La sérialisation peut être soit byte, json, xml ou simple toString, etc. ToString semble être moins cher. Lombok génère pour nous un ToSTring personnalisable facile et gratuit. Voir l'exemple ci-dessous.

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

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.