it-swarm.com.de

Gibt es ein Java-Reflektionsprogramm, um zwei Objekte tief zu vergleichen?

Ich versuche, Unit-Tests für eine Vielzahl von clone()-Vorgängen in einem großen Projekt zu schreiben, und ich frage mich, ob es irgendwo eine vorhandene Klasse gibt, die in der Lage ist, zwei Objekte desselben Typs aufzunehmen, einen tiefen Vergleich durchzuführen und wenn sind sie identisch oder nicht?

84
Uri

Unitils hat diese Funktionalität:

Gleichheitssicherung durch Reflektion, mit verschiedenen Optionen wie dem Ignorieren von Java-Standard-/Nullwerten und der Reihenfolge der Sammlungen

60
Wolfgang

Ich liebe diese Frage! Hauptsächlich, weil es kaum jemals beantwortet oder schlecht beantwortet wird. Es ist, als hätte es noch niemand herausgefunden. Neuland :)

Zunächst einmal nicht einmal denken über die Verwendung von equals. Der im javadoc definierte Vertrag von equals ist eine Äquivalenzrelation (reflexiv, symmetrisch und transitiv), nicht eine Gleichheitsrelation. Dafür müsste es auch antisymmetrisch sein. Die einzige Implementierung von equals, die eine echte Gleichheitsrelation ist (oder jemals sein könnte), ist die in Java.lang.Object. Selbst wenn Sie equals verwendet haben, um alles in der Grafik zu vergleichen, ist das Risiko eines Vertragsbruchs recht hoch. Wie Josh Bloch in Effective Java hervorhob, ist es sehr einfach, den Gleichheitsvertrag zu brechen:

"Es gibt einfach keine Möglichkeit, eine instanziierbare Klasse zu erweitern und einen Aspekt hinzuzufügen, während der Gleichstellungsvertrag beibehalten wird."

Abgesehen davon, was bringt Ihnen eine boolesche Methode überhaupt? Es wäre schön, alle Unterschiede zwischen dem Original und dem Klon zusammenzufassen, finden Sie nicht auch? Außerdem gehe ich hier davon aus, dass Sie nicht mit dem Schreiben/Verwalten von Vergleichscode für jedes Objekt im Diagramm beschäftigt sein möchten, sondern nach etwas suchen, das sich mit der Quelle skalieren lässt, wenn sich diese mit der Zeit ändert.

Soooo, was Sie wirklich wollen, ist eine Art Statusvergleichstool. Wie dieses Tool implementiert wird, hängt von der Art Ihres Domain-Modells und Ihren Leistungseinschränkungen ab. Nach meiner Erfahrung gibt es keine magische Kugel. Und es wird über eine große Anzahl von Iterationen hinweg langsam sein. Aber um die Vollständigkeit einer Klonoperation zu testen, wird es die Arbeit ziemlich gut machen. Ihre beiden besten Optionen sind Serialisierung und Reflektion.

Einige Probleme, auf die Sie stoßen werden:

  • Sammlungsreihenfolge: Sollen zwei Sammlungen als ähnlich betrachtet werden, wenn sie dieselben Objekte enthalten, jedoch in einer anderen Reihenfolge?
  • Welche Felder sind zu ignorieren: Transient? Statisch?
  • Typäquivalenz: Sollen Feldwerte exakt vom gleichen Typ sein? Oder ist es in Ordnung, wenn einer den anderen verlängert?
  • Es gibt noch mehr, aber ich vergesse ...

XStream ist ziemlich schnell und in Kombination mit XMLUnit erledigt es die Aufgabe in nur wenigen Codezeilen. XMLUnit ist nett, weil es alle Unterschiede melden kann oder einfach beim ersten anhält, den es findet. Und seine Ausgabe enthält den xpath zu den verschiedenen Knoten, was Nice ist. Standardmäßig sind ungeordnete Sammlungen nicht zulässig, dies kann jedoch konfiguriert werden. Durch das Injizieren eines speziellen Differenzen-Handlers (als DifferenceListener bezeichnet) können Sie festlegen, wie Sie mit Differenzen umgehen möchten, einschließlich des Ignorierens der Reihenfolge. Sobald Sie jedoch etwas anderes als die einfachste Anpassung vornehmen möchten, wird das Schreiben schwierig und die Details sind in der Regel an ein bestimmtes Domänenobjekt gebunden.

Ich persönlich bevorzuge es, alle deklarierten Felder mit Reflektion zu durchlaufen und jeden einzelnen aufzuspüren, um die Unterschiede zu verfolgen. Warnung: Verwenden Sie keine Rekursion, es sei denn, Sie möchten Stapelüberlauf-Ausnahmen. Halten Sie die Dinge in Reichweite mit einem Stapel (verwenden Sie ein LinkedList oder so). Normalerweise ignoriere ich transiente und statische Felder und überspringe bereits verglichene Objektpaare, damit ich nicht in Endlosschleifen gerate, wenn sich jemand dazu entschließt, selbstreferenziellen Code zu schreiben (ich vergleiche jedoch immer primitive Wrapper, egal was passiert) , da dieselben Objektreferenzen oft wiederverwendet werden). Sie können Dinge im Voraus so konfigurieren, dass die Reihenfolge der Sammlungen ignoriert und spezielle Typen oder Felder ignoriert werden. Ich definiere jedoch gerne meine Richtlinien für den Statusvergleich für die Felder selbst über Anmerkungen. Dies, IMHO, ist genau das, wofür Annotationen gedacht waren, um Metadaten über die Klasse zur Laufzeit verfügbar zu machen. So etwas wie:


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

Ich denke, das ist ein wirklich schweres Problem, aber völlig lösbar! Und wenn Sie etwas haben, das für Sie funktioniert, ist es wirklich sehr, sehr praktisch :)

Also viel Glück. Und wenn Sie etwas nur Geniales finden, vergessen Sie nicht, es mit anderen zu teilen!

27
Kevin C

Siehe DeepEquals und DeepHashCode () in Java-util: https://github.com/jdereg/Java-util

Diese Klasse macht genau das, was der ursprüngliche Autor anfordert.

12

Ich musste nur den Vergleich von zwei von Hibernate Envers überarbeiteten Entitätsinstanzen implementieren. Ich fing an, meinen eigenen Unterschied zu schreiben, fand dann aber folgende Rahmenbedingungen.

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

Sie können zwei Objekte desselben Typs vergleichen und Änderungen, Ergänzungen und Entfernungen anzeigen. Wenn es keine Änderungen gibt, sind die Objekte (theoretisch) gleich. Anmerkungen werden für Getter bereitgestellt, die bei der Überprüfung ignoriert werden sollten. Das Rahmenwerk hat weitaus breitere Anwendungen als die Gleichheitsprüfung, d. H. Ich verwende ein Änderungsprotokoll.

Die Leistung ist in Ordnung. Wenn Sie JPA-Entitäten vergleichen, müssen Sie diese zuerst vom Entitätsmanager trennen.

7
Ryan

Ich verwende 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();
}
6
G. Weiss

Überschreiben Sie die Methode equals ()

Sie können die equals () - Methode der Klasse einfach mit EqualsBuilder.reflectionEquals () überschreiben, wie erklärt hier :

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

In AssertJ können Sie Folgendes tun:

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

Wahrscheinlich funktioniert es nicht in allen Fällen, aber es funktioniert in mehr Fällen, als Sie denken.

Folgendes sagt die Dokumentation:

Stellen Sie fest, dass das zu testende Objekt (IST) dem angegebenen .__ entspricht. Objekt basierend auf rekursiver Eigenschaft/Feld nach Eigenschaft/Feld Vergleich (einschließlich vererbter). Dies kann nützlich sein, wenn Gleiche Implementierung passt nicht zu Ihnen. Die rekursive Eigenschaft/das Feld Der Vergleich wird nicht auf Felder angewendet, die eine benutzerdefinierte Gleichheit haben Implementierung, d. h. stattdessen wird die überschriebene Methode equals verwendet. eines Feldes durch Feldvergleich.

Der rekursive Vergleich behandelt Zyklen. Floats sind standardmäßig verglichen mit einer Genauigkeit von 1,0E-6 und verdoppelt sich mit 1,0E-15.

Sie können einen benutzerdefinierten Vergleicher für (verschachtelte) Felder angeben oder mit .__ eingeben. usingComparatorForFields (Comparator, String ...) und usingComparatorForType (Comparator, Class).

Die zu vergleichenden Objekte können unterschiedlichen Typs sein, müssen jedoch die gleiche Eigenschaften/Felder. Zum Beispiel, wenn das tatsächliche Objekt einen Namen hat. String In diesem Feld wird erwartet, dass das andere Objekt auch ein Objekt hat. Wenn ein Objekt hat ein Feld und eine Eigenschaft mit demselben Namen, der Eigenschaftswert wird über dem Feld verwendet werden.

2
Vlad Dinulescu

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
gavenkoa

Wenn Ihre Objekte Serializable implementieren, können Sie Folgendes verwenden:

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);
    }
}
2
Ray Hulha

Ihr Beispiel für eine verknüpfte Liste ist nicht so schwer zu handhaben. Während der Code die beiden Objektdiagramme durchquert, werden besuchte Objekte in einem Set oder einer Map platziert. Vor dem Einfahren in eine andere Objektreferenz wird dieser Satz daraufhin geprüft, ob das Objekt bereits verfahren wurde. Wenn ja, müssen Sie nicht weiter gehen.

Ich stimme mit der Person überein, die sagte, dass sie eine LinkedList verwenden soll (wie einen Stack, aber ohne synchronisierte Methoden, also ist es schneller). Das Durchlaufen des Objektdiagramms mit einem Stapel und die Verwendung der Reflexion für jedes Feld ist die ideale Lösung. Einmal geschrieben, dieses "externe" equals () und "externe" hashCode () ist das, was alle equals () - und hashCode () - Methoden aufrufen sollten. Nie wieder brauchen Sie eine Customer-Equals-Methode.

Ich habe etwas Code geschrieben, der ein komplettes Objektdiagramm durchquert, das bei Google Code aufgelistet ist. Siehe json-io (http://code.google.com/p/json-io/). Es serialisiert einen Java-Objektgraph in JSON und deserialisiert ihn. Es behandelt alle Java-Objekte, mit oder ohne öffentliche Konstruktoren, serialisierbar oder nicht serialisierbar usw. Dieser gleiche Durchlaufcode bildet die Grundlage für die externe Implementierung "equals ()" und externe "hashcode ()". Übrigens, der JsonReader/JsonWriter (Json-Io) ist normalerweise schneller als der eingebaute ObjectInputStream/ObjectOutputStream.

Dieser JsonReader/JsonWriter kann zum Vergleich verwendet werden, er hilft jedoch nicht beim Hashcode. Wenn Sie einen universellen Hashcode () und Equals () wünschen, ist ein eigener Code erforderlich. Ich kann dies mit einem generischen Graphbesucher durchführen. Wir werden sehen.

Andere Überlegungen - statische Felder - das ist einfach - sie können übersprungen werden, da alle equals () -Instanzen für statische Felder denselben Wert haben würden, da die statischen Felder von allen Instanzen gemeinsam genutzt werden.

Wie bei vorübergehenden Feldern ist dies eine auswählbare Option. Manchmal möchten Sie, dass Transienten andere Zeiten nicht zählen. "Manchmal fühlst du dich wie eine Nuss, manchmal nicht."

Schauen Sie sich das json-io-Projekt an (für meine anderen Projekte) und Sie finden das externe equals ()/hashcode () -Projekt. Ich habe noch keinen Namen dafür, aber es wird offensichtlich sein.

1

Apache gibt Ihnen etwas, konvertiert beide Objekte in einen String und vergleicht Strings, aber toString () muss überschrieben werden.

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

ToString () überschreiben

Wenn alle Felder primitive Typen sind:

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

Wenn Sie nicht-primitive Felder und/oder eine Sammlung und/oder eine Karte haben:

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

Hamcrest hat den Matcher samePropertyValuesAs . Es beruht jedoch auf der JavaBeans-Konvention (verwendet Getter und Setter). Wenn für die zu vergleichenden Objekte keine Attribute und Setter vorhanden sind, funktioniert dies nicht.

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

Die User-Bean - mit Gettern und 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;
    }

}
1
cheffe

Eine ausbleibende Garantie für einen derart tiefen Vergleich könnte ein Problem darstellen. Was soll das Folgende tun? (Wenn Sie einen solchen Komparator implementieren, wäre dies ein guter Komponententest.)

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

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

Hier ist ein anderes:

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

System.out.println(DeepCompare(c, d));
0
Ben Voigt

Ich denke, die einfachste Lösung, die durch die Ray Hulha-Lösung inspiriert wurde, besteht darin, das Objekt zu serialisieren und dann das Rohergebnis zu vergleichen.

Die Serialisierung könnte entweder Byte, Json, XML oder einfach zu String usw. sein. ToString scheint billiger zu sein. Lombok generiert kostenlos ein einfach anpassbares ToSTring für uns. Siehe Beispiel unten.

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

Ich denke, Sie wissen das, aber theoretisch sollten Sie Äquivalente immer überschreiben, um zu behaupten, dass zwei Objekte wirklich gleich sind. Dies würde bedeuten, dass sie die überschriebenen .equals-Methoden ihrer Mitglieder überprüfen.

Aus diesem Grund werden in Object .quivalente definiert.

Wenn Sie dies konsequent machen würden, hätten Sie kein Problem.

0
Bill K