web-dev-qa-db-ja.com

2つのオブジェクトの詳細な比較を行うためのJavaリフレクションユーティリティはありますか?

私は大規模なプロジェクト内のさまざまなclone()操作の単体テストを作成しようとしていますが、同じタイプの2つのオブジェクトを取得できる既存のクラスがあるかどうか疑問に思っています深い比較、およびそれらが同一であるかどうかを言っていますか?

89
Uri

nitils には次の機能があります。

Java default/null値を無視し、コレクションの順序を無視するなどのさまざまなオプションを使用した、リフレクションによる等価アサーション

61
Wolfgang

この質問が大好きです!主に、それがほとんど答えられないか、ひどく答えられるからです。まだ誰もそれを理解していないようです。処女の領土:)

最初に、equalsの使用について思考でもしないでください。 javadocで定義されているequalsのコントラクトは、同値関係(再帰的、対称的、推移的)、not等式関係です。そのためには、非対称である必要もあります。 equalsの唯一の実装は、真の等式関係である(または可能な限り)Java.lang.Objectequalsを使用してグラフ内のすべてを比較したとしても、契約を破るリスクは非常に高くなります。 Josh BlochがEffective Javaで指摘したように、equalsの契約は非常に簡単に破ることができます。

「インスタンス化可能なクラスを拡張し、equalsコントラクトを保持しながらアスペクトを追加する方法はありません」

とにかく、ブール値メソッドは本当にあなたに何をするのですか?元のクローンとクローンの違いをすべて実際にカプセル化するのは良いことだと思いませんか?また、ここでは、グラフ内の各オブジェクトの比較コードを記述/維持することに煩わされたくないと仮定しますが、ソースが時間とともに変化するにつれてスケーリングするものを探していると仮定します。

本当に欲しいのは、ある種の状態比較ツールです。このツールの実装方法は、ドメインモデルの性質とパフォーマンスの制限に依存します。私の経験では、一般的な魔法の弾丸はありません。そして、それはwill多数の反復にわたって遅くなります。しかし、クローン操作の完全性をテストするためには、かなりうまく機能します。 2つの最適なオプションは、シリアル化とリフレクションです。

発生するいくつかの問題:

  • コレクションの順序:2つのコレクションは、同じオブジェクトを保持しているが、順序が異なる場合、類似していると見なされるべきですか?
  • 無視するフィールド:一時的?静的?
  • 型の等価性:フィールド値はまったく同じ型である必要がありますか?それとも、一方が他方を拡張しても大丈夫ですか?
  • 他にもありますが、忘れてしまいました...

XStreamは非常に高速で、XMLUnitと組み合わせることで、わずか数行のコードで作業を実行できます。 XMLUnitは、すべての違いを報告できるか、最初に見つかったもので停止することができるため、素晴らしいです。その出力には、異なるノードへのxpathが含まれています。これはNiceです。デフォルトでは、順序付けられていないコレクションを許可しませんが、許可するように構成できます。特別な差異ハンドラー(DifferenceListenerと呼ばれる)を挿入すると、順序を無視するなど、差異に対処する方法を指定できます。ただし、最も単純なカスタマイズ以外のことを行うとすぐに、記述が難しくなり、詳細が特定のドメインオブジェクトに結び付けられる傾向があります。

私の個人的な好みは、リフレクションを使用して、宣言されたすべてのフィールドを循環し、各フィールドにドリルダウンして、違いを追跡することです。警告の言葉:スタックオーバーフローの例外を好まない限り、再帰を使用しないでください。スタックでスコープ内に物を置いてください(LinkedListまたは何かを使用してください)。私は通常、一時フィールドと静的フィールドを無視し、既に比較したオブジェクトペアをスキップするため、誰かが自己参照コードを記述することにした場合、無限ループに陥ることはありません(ただし、私は常にプリミティブラッパーを比較します、同じオブジェクト参照が頻繁に再利用されるため)。コレクションの順序を無視し、特別なタイプまたはフィールドを無視するように事前に構成できますが、アノテーションを介してフィールド自体の状態比較ポリシーを定義するのが好きです。これは、IMHOであり、実行時にクラスに関するメタデータを利用できるようにするための注釈の目的です。何かのようなもの:


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

これは実際には非常に難しい問題ですが、完全に解決できると思います!そして、あなたに合ったものがあれば、それは本当に、本当に、便利です:)

とても幸運。そして、あなたが純粋な天才である何かを思いついたら、共有することを忘れないでください!

27
Kevin C

Java-util内のDeepEqualsおよびDeepHashCode()を参照してください: https://github.com/jdereg/Java-util

このクラスは、元の作者が要求することを正確に行います。

13

Equals()メソッドをオーバーライドします

説明したようにEqualsBuilder.reflectionEquals()を使用して、クラスのequals()メソッドを単純にオーバーライドできます こちら

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

Hibernate Enversによって改訂された2つのエンティティインスタンスの比較を実装する必要がありました。私は自分とは違うものを書き始めましたが、次のフレームワークを見つけました。

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

同じタイプの2つのオブジェクトを比較すると、変更、追加、削除が表示されます。変更がない場合、オブジェクトは等しくなります(理論上)。チェック中に無視されるゲッター用の注釈が提供されます。フレームワークは、同等性チェックよりもはるかに広い用途があります。つまり、変更ログを生成するために使用しています。

JPAエンティティを比較する場合、そのパフォーマンスは問題ありません。最初にエンティティマネージャからそれらを切り離してください。

7
Ryan

私は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

AssertJ では、次のことができます。

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

おそらくすべてのケースで機能するわけではありませんが、あなたが思うより多くのケースで機能します。

ドキュメントの内容は次のとおりです。

テスト中のオブジェクト(実際)が、プロパティ/フィールド比較(継承されたものを含む)によるプロパティ/フィールドの再帰に基づいて、指定されたオブジェクトと等しいことをアサートします。これは、actualのequals実装があなたに合わない場合に便利です。再帰的なプロパティ/フィールド比較は、カスタムのequals実装を持つフィールドには適用されません。つまり、フィールドごとの比較の代わりにオーバーライドされたequalsメソッドが使用されます。

再帰的比較はサイクルを処理します。デフォルトでは、フロートは1.0E-6の精度と比較され、1.0E-15の倍精度と比較されます。

(ネストされた)フィールドまたはタイプごとにカスタムコンパレーターを指定するには、それぞれusingComparatorForFields(Comparator、String ...)およびusingComparatorForType(Comparator、Class)を使用します。

比較するオブジェクトは異なるタイプにすることができますが、同じプロパティ/フィールドを持つ必要があります。たとえば、実際のオブジェクトに名前文字列フィールドがある場合、他のオブジェクトにも名前フィールドがあることが期待されます。オブジェクトに同じ名前のフィールドとプロパティがある場合、プロパティ値はフィールドで使用されます。

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

オブジェクトがSerializableを実装している場合、これを使用できます。

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

リンクリストの例は、それほど難しくありません。コードは2つのオブジェクトグラフを走査するときに、訪問したオブジェクトをSetまたはMapに配置します。別のオブジェクト参照をトラバースする前に、このセットをテストして、オブジェクトがすでにトラバースされているかどうかを確認します。もしそうなら、さらに進む必要はありません。

LinkedListを使用すると言った上記の人に同意します(Stackに似ていますが、同期メソッドがないため、高速です)。スタックを使用してオブジェクトグラフを走査し、反射を使用して各フィールドを取得することが理想的なソリューションです。一度書かれたこの「外部」equals()および「外部」hashCode()は、すべてのequals()およびhashCode()メソッドが呼び出すべきものです。顧客のequals()メソッドはもう必要ありません。

Google Codeにリストされている完全なオブジェクトグラフを横断するコードを少し書きました。 json-io(http://code.google.com/p/json-io/)を参照してください。 JavaオブジェクトグラフをJSONにシリアル化し、そこからデシリアライズします。すべてのJavaオブジェクト、パブリックコンストラクターの有無にかかわらず、Serializeableまたはnot Serializableなどを処理します。この同じトラバーサルコードは、外部の "equals()"および外部の "hashcode()"実装の基礎になります。

このJsonReader/JsonWriterは比較に使用できますが、ハッシュコードには役立ちません。ユニバーサルhashcode()とequals()が必要な場合は、独自のコードが必要です。一般的なグラフビジターでこれを実行できる場合があります。表示します。

その他の考慮事項-静的フィールド-それは簡単です-静的フィールドはすべてのインスタンスで共有されるため、すべてのequals()インスタンスは静的フィールドに対して同じ値を持つため、スキップできます。

一時的なフィールドについては-選択可能なオプションになります。トランジェントが他の時間をカウントしないようにしたい場合があります。 「時々気分が悪くなることもあるが、気が進まないこともある。」

Json-ioプロジェクト(他のプロジェクト用)に戻ると、外部のequals()/ hashcode()プロジェクトが見つかります。まだ名前はありませんが、明らかです。

1

Apacheは何かを与え、両方のオブジェクトを文字列に変換し、文字列を比較しますが、toString()をオーバーライドする必要があります

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

ToString()をオーバーライドします

すべてのフィールドがプリミティブ型の場合:

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

プリミティブでないフィールドおよび/またはコレクションおよび/またはマップがある場合:

// 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には、Matcher samePropertyValuesAs があります。ただし、JavaBeansコンベンションに依存しています(ゲッターとセッターを使用)。比較するオブジェクトに属性のゲッターとセッターがない場合、これは機能しません。

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

ユーザーBean-ゲッターとセッターを使用

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

このような詳細な比較の停止保証は問題になる可能性があります。以下は何をすべきですか? (このようなコンパレータを実装すると、これは優れた単体テストになります。)

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

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

ここに別のものがあります:

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

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

Ray Hulha solutionに触発された最も簡単な解決策は、オブジェクトをシリアル化し、生の結果を詳細に比較することだと思います。

シリアル化は、バイト、json、xml、または単純なtoStringなどのいずれかです。ToStringはより安価であるようです。 Lombokは、無料で簡単にカスタマイズ可能なToSTringを生成します。以下の例を参照してください。

@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.

あなたはこれを知っていると思いますが、理論的には、.equalsを常にオーバーライドして、2つのオブジェクトが本当に等しいと断言することになっています。これは、メンバーのオーバーライドされた.equalsメソッドをチェックすることを意味します。

このようなことが、オブジェクトで.equalsが定義されている理由です。

これが一貫して行われていれば、問題はありません。

0
Bill K