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


99

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


1
このクラスは、オブジェクトグラフの特定の時点で、同じオブジェクトを受け入れることができるのか、それとも同じ参照のみを受け入れるのかをどのようにして知るのでしょうか?
ゼッド

理想的には、十分に構成可能である:)新しいフィールドが追加された場合(およびクローンされていない場合)、テストでそれらを識別できるように、何か自動のものを探しています。
Uri

3
私が言おうとしているのは、とにかく比較を構成する(つまり実装する)必要があるということです。それでは、クラスのequalsメソッドをオーバーライドして、それを使用してみませんか?
Zed

3
大きな複雑なオブジェクトに対してequalsがfalseを返す場合、どこから始めますか?オブジェクトを複数行の文字列に変換し、文字列比較を行う方がはるかに優れています。次に、2つのオブジェクトが異なる場所を正確に確認できます。IntelliJは、2つの結果間の複数の変更を見つけるのに役立つ「変更」比較ウィンドウをポップアップします。つまり、assertEquals(string1、string2)の出力を理解し、比較ウィンドウを提供します。
Peter Lawrey 09/10/30

受け入れられたもの以外にも、ここには本当に良い答えがいくつかあります。それは埋められたようです
user1445967

回答:


62

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

リフレクションによる等価アサーション。Javaのデフォルト/ null値の無視やコレクションの順序の無視など、さまざまなオプションがあります。


9
私はこの関数でいくつかのテストを行いましたが、EqualsBuilderとは異なり、深い比較を行っているようです。
ハワード5月

これが一時的なフィールドを無視しないようにする方法はありますか?
2016

@ピンチ私はあなたを聞きます。unitils変数が観察可能な影響を及ぼさない場合でも変数を比較するため、のディープ比較ツールには欠陥があります。変数を比較した場合のもう1つの(望ましくない)結果は、(独自の状態のない)純粋なクロージャーがサポートされないことです。さらに、比較するオブジェクトが同じランタイム型である必要があります。私は自分の袖をまくって、これらの懸念に対処する独自のバージョンの詳細比較ツールを作成しました。
beluchin

@Wolfgangに指示するサンプルコードはありますか?その引用をどこから引き出しましたか?
anon58192932 2017

30

この質問が大好きです!主にそれがほとんど答えられないか、または悪い答えをされたからです。まだ誰もそれを理解していないようです。バージンテリトリー:)

まず、の使用については考えないでくださいequals。の規約はequals、javadocで定義されているように、等価関係(再帰的、対称的、推移的)であり、等価関係ではありません。そのためには、反対称である必要もあります。これの唯一の実装はequals、真の等価関係です(またはそうである可能性があります)java.lang.Objectequalsグラフのすべてを比較するために使用したとしても、契約を破るリスクは非常に高くなります。Josh BlochがEffective Javaで指摘したように、イコールの契約は破るのが非常に簡単です:

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

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

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

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

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

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

私の個人的な好みは、リフレクションを使用して、宣言されたすべてのフィールドを循環し、各フィールドにドリルダウンして、差分を追跡することです。警告の言葉:スタックオーバーフロー例外が好きでない限り、再帰を使用しないでください。スタックを使用して対象物を保持します(LinkedListか何か)。私は通常、一時フィールドと静的フィールドを無視し、すでに比較したオブジェクトのペアをスキップするので、誰かが自己参照コードを書くことに決めた場合に無限ループに陥ることはありません(ただし、常にプリミティブラッパーを比較します、同じオブジェクト参照がしばしば再利用されるため)。コレクションの順序を無視したり、特殊なタイプやフィールドを無視したりするように前もって設定できますが、アノテーションを使用してフィールド自体の状態比較ポリシーを定義したいと思います。これがIMHOであり、クラスのメタデータを実行時に利用できるようにするために、アノテーションが意図したものです。何かのようなもの:


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

これは本当に難しい問題だと思いますが、完全に解決できます!そして、あなたのために機能するものがあれば、それは本当に、本当に、便利です:)

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


15

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

このクラスは、元の作者が要求したとおりのことを行います。


4
警告:オブジェクトが存在する場合、DeepEqualsはオブジェクトの.equals()メソッドを使用します。これはあなたが望むものではないかもしれません。
Adam

4
equals()メソッドが明示的に追加された場合にのみ、クラスで.equals()を使用します。それ以外の場合は、メンバーごとの比較を行います。ここでのロジックは、誰かがカスタムのequals()メソッドを作成しようと試みた場合、それを使用する必要があるということです。将来の拡張:equals()メソッドが存在する場合でも無視するようフラグに許可します。CaseInsensitiveMap / Setなど、java-utilには便利なユーティリティがあります。
John DeRegnaucourt 2014年

フィールドの比較が心配です。フィールドの違いは、オブジェクトのクライアントの観点からは観察できない場合がありますが、フィールドに基づく詳細な比較ではフラグが立てられます。さらに、フィールドを比較するには、オブジェクトが同じランタイムタイプである必要があります。
ベルチン

上記の@beluchinに答えるために、DeepEquals.deepEquals()は常にフィールドごとの比較を行うわけではありません。まず、メソッドに.equals()が存在する場合(それがObjectにない場合)を使用するか、無視するかを選択できます。次に、マップ/コレクションを比較するときに、コレクションまたはマップタイプ、またはコレクション/マップのフィールドは参照されません。代わりに、それらを論理的に比較します。LinkedHashMapは、同じコンテンツと要素を同じ順序で持っている場合、TreeMapと同じになります。順序付けられていないコレクションとマップの場合、必要なのはサイズと深さが等しい項目のみです。
John DeRegnaucourt

マップ/コレクションを比較する場合、コレクションまたはマップタイプも、コレクション/マップのフィールドも参照しません。代わりに、 @ JohnDeRegnaucourtを論理的に比較します。この論理的な比較、つまりpublic、コレクション/マップにのみ適用できるのではなく、すべてのタイプに適用する必要があるものだけを比較することを主張します。
beluchin

10

equals()メソッドをオーバーライドする

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

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

7

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

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

同じタイプの2つのオブジェクトを比較すると、変更、追加、削除が表示されます。変更がない場合、オブジェクトは(理論的には)等しくなります。チェック中に無視する必要があるゲッター用のアノテーションが用意されています。フレームワークには、同等性チェックよりもはるかに広いアプリケーションがあります。つまり、変更ログの生成に使用しています。

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


6

私は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
リスト以外のコレクションは異なる順序で要素を返す可能性があるため、文字列の比較は失敗します。
Alexey Berezkin 14

また、シリアル化できないクラスは失敗します
Zwelch

6

AssertJ、あなたが行うことができます。

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

おそらくそれはすべてのケースでうまくいくわけではありませんが、あなたが考えるより多くのケースでうまくいくでしょう。

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

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

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

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

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


1
isEqualToComparingFieldByFieldRecursivelyは非推奨になりました。assertThat(expectedObject).usingRecursiveComparison().isEqualTo(actualObject);代わりに使用してください:)
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
生成されたクラスを処理する必要があり、イコールについて影響がない場合に特に便利です。
Matthias B

1
stackoverflow.com/a/1449051/829755はこれについて既に言及しています。あなたはその投稿を編集するべき
でした

1
@ user829755このようにしてポイントを失います。だから、ポイントゲームのすべて))人々は仕事が終わったときにクレジットを取得したいのですが、私もそうです。
Givenkoa 2014年

3

Hamcrestにはマッチャー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;
    }

}

プロパティのisFoo読み取りメソッドを使用しているPOJOができるまで、これはうまく機能しBooleanます。それを修正するために2016年以来開かれているPRがあります。 github.com/hamcrest/JavaHamcrest/pull/136
Snekse

2

オブジェクトが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);
    }
}

1

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

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

私は、完全なオブジェクトグラフを横断するコードを少し書いて、Google Codeにリストしました。json-io(http://code.google.com/p/json-io/)を参照してください。JavaオブジェクトグラフをJSONにシリアル化し、そこから逆シリアル化します。パブリックコンストラクターの有無にかかわらず、SerializeableまたはNot SerializableなどのすべてのJavaオブジェクトを処理します。この同じトラバーサルコードは、外部の「equals()」および外部の「hashcode()」実装の基礎になります。ところで、JsonReader / JsonWriter(json-io)は通常、組み込みのObjectInputStream / ObjectOutputStreamよりも高速です。

この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--;
        }
    }
}}

0

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

この種のことが、.equalsがObjectで定義されている理由です。

これが一貫して行われた場合、問題は発生しません。


2
問題は、私が記述していない大規模な既存のコードベースに対してこれをテストすることを自動化したいことです... :)
Uri

0

このような深い比較の停止保証は問題になるかもしれません。次は何をすべきですか?(このようなコンパレーターを実装すると、これは優れた単体テストになります。)

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

新しい質問がある場合は、[ 質問する ]ボタンをクリックして質問してください。コンテキストの提供に役立つ場合は、この質問へのリンクを含めます。
YoungHobbit

@younghobbit:いいえ、これは新しい質問ではありません。回答に疑問符が付いていても、そのフラグは適切ではありません。もっと注意してください。
Ben Voigt

これから:Using an answer instead of a comment to get a longer limit and better formatting.これがコメントの場合、なぜ回答セクションを使用するのですか?そのため、フラグを立てました。のせいではない?。この回答は、コメントを残さなかった他のユーザーによって既にフラグが付けられています。これをレビューキューに入れました。私の悪いことかもしれませんが、もっと注意する必要がありました。
YoungHobbit 2015年

0

Ray Hulhaソリューションに触発された最も簡単なソリューションだと思いますは、オブジェクトをシリアル化してから、生の結果を深く比較する。

シリアライゼーションは、バイト、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());
    }
}   

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.