101

テストでフィールド値に基づいて equals メソッドを実装していない 2 つのオブジェクトを「深く」比較する方法は?


元の質問 (精度が不足しているため、SO 基準を満たしていないためクローズされました)、文書化の目的で保持されています。

大規模なプロジェクト内のさまざまな操作の単体テストを作成しようとしていclone()ますが、同じタイプの 2 つのオブジェクトを取得し、詳細な比較を行い、それらが同一かどうか?

4

15 に答える 15

64

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

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

于 2009-09-19T17:34:30.527 に答える
31

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

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

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

とにかく、ブールメソッドは本当に何の役に立つのでしょうか? オリジナルとクローンのすべての違いを実際にカプセル化できたらいいですね。また、ここでは、グラフ内の各オブジェクトの比較コードを作成/維持することに煩わされたくなく、時間の経過とともに変化するソースに合わせてスケーリングするものを探していると仮定します。

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

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

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

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

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


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

これは実際には非常に難しい問題だと思いますが、完全に解決可能です! そして、自分に合ったものを手に入れたら、それは本当に、本当に、便利です:)

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

于 2010-09-30T03:14:13.790 に答える
15

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

このクラスは、元の作成者が要求したことを正確に実行します。

于 2012-04-25T23:59:24.160 に答える
7

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

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

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

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

于 2012-09-17T05:24:03.397 に答える
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);
于 2013-02-12T07:21:54.780 に答える
5

私は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();
}
于 2009-09-29T20:10:13.083 に答える
3

Hamcrest には Matcher samePropertyValuesAsがあります。ただし、JavaBeans Convention に依存しています (getter と setter を使用します)。比較対象のオブジェクトに属性のゲッターとセッターがない場合、これは機能しません。

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

}
于 2015-06-29T06:46:07.163 に答える
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);
    }
}
于 2015-10-05T18:25:22.953 に答える
1

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

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

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

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

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

一時的なフィールドについては、選択可能なオプションになります。場合によっては、トランジェントを他の回数をカウントしないようにしたい場合があります。「気が狂ったように感じるときもあれば、そうでないときもある。」

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

于 2011-03-25T20:47:24.153 に答える
0

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

.equals が Object で定義されているのは、このような理由からです。

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

于 2009-09-29T20:21:52.350 に答える
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));
于 2010-09-30T03:22:16.487 に答える