是否有Java反射实用程序来对两个对象进行深度比较?

时间:2009-09-19 17:14:27

标签: java comparison equals

我正在尝试为大型项目中的各种clone()操作编写单元测试,我想知道是否存在一个能够获取两个相同类型对象的现有类,深入比较,并说是否相同?

16 个答案:

答案 0 :(得分:60)

Unitils具有此功能:

  

通过反射进行平等断言,使用不同的选项,例如忽略Java默认值/空值并忽略集合的顺序

答案 1 :(得分:27)

我喜欢这个问题!主要是因为它几乎没有得到很好的回答或回答。这就像没有人想出来的那样。处女地区:)

首先,不要认为使用equals。 javadoc中定义的equals的契约是等价关系(自反,对称和传递),是一个等式关系。为此,它也必须是反对称的。 equals中{(1}}的唯一实现是(或可能是)真正的平等关系。java.lang.Object中的实现。即使您确实使用equals来比较图表中的所有内容,违约的风险也非常高。正如Josh Bloch在 Effective Java 中指出的那样,平等合同很容易打破:

“在保留equals合同的同时,根本无法扩展可实例化的类并添加方面”

除了布尔方法有什么好处之外你呢?真的很好地封装原始和克隆之间的所有差异,你不觉得吗?另外,我在这里假设您不想为图中的每个对象编写/维护比较代码而烦恼,而是在寻找随着时间的推移而随着源变化而扩展的内容。< / p>

Soooo,你真正想要的是某种状态比较工具。该工具的实现方式实际上取决于域模型的性质和性能限制。根据我的经验,没有通用的魔术子弹。并且在大量迭代中变慢。但是为了测试克隆操作的完整性,它将很好地完成工作。你最好的两个选择是序列化和反思。

您将遇到的一些问题:

  • 集合顺序:如果两个集合拥有相同的对象,但它们的顺序不同,是否应该被视为相似?
  • 忽略哪些字段:瞬态?静态?
  • 类型等价:字段值应该是完全相同的类型吗?或者是否可以扩展另一个?
  • 还有更多,但我忘了......

XStream非常快,与XMLUnit相结合只需几行代码即可完成。 XMLUnit很不错,因为它可以报告所有差异,或者只是停在它找到的第一个差异上。它的输出包括不同节点的xpath,这很好。默认情况下,它不允许无序集合,但可以配置为执行此操作。注入特殊差异处理程序(称为DifferenceListener)允许您指定处理差异的方式,包括忽略顺序。但是,只要您想要执行除最简单的自定义之外的任何操作,就会很难编写,并且细节往往会绑定到特定的域对象。

我个人的偏好是使用反射循环遍历所有声明的字段并向下钻取每个字段,跟踪差异。警告:除非您喜欢堆栈溢出异常,否则不要使用递归。使用堆栈保存范围(使用LinkedList或其他内容)。我通常忽略瞬态和静态字段,并且我跳过我已经比较的对象对,所以如果有人决定编写自引用代码,我不会在无限循环中结束(但是,我总是比较原始包装器,不管是什么,因为相同的对象引用经常被重用)。您可以预先配置以忽略集合排序并忽略特殊类型或字段,但我喜欢通过注释在字段本身上定义状态比较策略。恕我直言,这正是注释的意思,使运行时可以获得有关类的元数据。类似的东西:


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

我认为这实际上是一个非常难的问题,但完全可以解决!一旦你有适合自己的东西,这真的非常方便:)

所以,祝你好运。如果你想出的东西只是纯粹的天才,别忘了分享!

答案 2 :(得分:13)

请参阅java-util中的DeepEquals和DeepHashCode():https://github.com/jdereg/java-util

这个类完全符合原作者的要求。

答案 3 :(得分:7)

只需要实现Hibernate Envers修改的两个实体实例的比较。我开始写自己的不同,但后来找到了以下框架。

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

您可以比较两个相同类型的对象,它将显示更改,添加和删除。如果没有变化,则对象相等(理论上)。为检查期间应忽略的getter提供注释。框架工作比等式检查有更广泛的应用,即我用来生成更改日志。

在比较JPA实体时,它的性能还可以,请务必先将它们与实体管理器分离。

答案 4 :(得分:7)

覆盖equals()方法

您可以使用 EqualsBuilder.reflectionEquals()覆盖类的 equals()方法,如{{3}}所述:

f.write(soup.get_text())

答案 5 :(得分: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();
}

答案 6 :(得分:3)

AssertJ中,您可以执行以下操作:

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

可能并非在所有情况下都有效,但是在您认为更多的情况下它将适用。

文档内容如下:

  

断言被测对象(实际)等于给定对象   基于属性/字段的递归的对象   比较(包括继承的)。如果实际的   等于实施不适合您。递归属性/字段   比较不适用于具有自定义等于的字段   实现,即将改用覆盖的equals方法   通过字段比较确定一个字段。

     

递归比较处理循环。默认情况下,浮点数是   精度为1.0E-6,而精度为1.0E-15。

     

您可以按(嵌套)字段指定自定义比较器,也可以输入   分别使用ComparatorForFields(Comparator,String ...)和   usingComparatorForType(Comparator,Class)。

     

要比较的对象可以是不同的类型,但必须具有   相同的属性/字段。例如,如果实际对象的名称为String   字段,期望另一个对象也有一个。如果有物体   有一个字段和一个具有相同名称的属性,该属性值将   在田间使用。

答案 7 :(得分:2)

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

答案 8 :(得分: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);
    }
}

答案 9 :(得分:1)

您的链接列表示例并不难处理。当代码遍历两个对象图时,它将访问的对象放置在Set或Map中。在遍历另一个对象引用之前,将测试此集以查看是否已遍历该对象。如果是这样,就不需要进一步了。

我同意上面说过使用LinkedList的人(比如Stack但没有使用synchronized方法,所以它更快)。使用堆栈遍历对象图,同时使用反射来获取每个字段,是理想的解决方案。写一次,这个“外部”等于()和“外部”hashCode()是所有equals()和hashCode()方法应该调用的。再也不需要客户equals()方法了。

我写了一些代码,遍历完整的对象图,列在Google Code上。请参阅json-io(http://code.google.com/p/json-io/)。它将Java对象图序列化为JSON并从中反序列化。它处理所有Java对象,有或没有公共构造函数,Serializeable或Serializable等。这个相同的遍历代码将是外部“equals()”和外部“hashcode()”实现的基础。顺便说一下,JsonReader / JsonWriter(json-io)通常比内置的ObjectInputStream / ObjectOutputStream更快。

这个JsonReader / JsonWriter可用于比较,但它对hashcode没有帮助。如果你想要一个通用的hashcode()和equals(),它需要它自己的代码。我可以用通用图形访问者来解决这个问题。我们会看到。

其他注意事项 - 静态字段 - 这很容易 - 可以跳过它们,因为所有equals()实例对于静态字段都具有相同的值,因为静态字段在所有实例中共享。

对于瞬态字段 - 这将是一个可选择的选项。有时您可能希望瞬态计数不是其他时间。 “有时候你感觉自己像个坚果,有时你却没有。”

回到json-io项目(对于我的其他项目),你会发现外部的equals()/ hashcode()项目。我还没有名字,但很明显。

答案 10 :(得分:1)

Hamcrest有匹配者samePropertyValuesAs。但它依赖于JavaBeans公约(使用getter和setter)。如果要比较的对象的属性没有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 - 带有getter和setter

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

}

答案 11 :(得分: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--;
        }
    }
}}

答案 12 :(得分:0)

我想你知道这一点,但理论上,你应该总是重写.equals来断言两个对象是真正相等的。这意味着他们会检查其成员上被覆盖的.equals方法。

这就是为什么.equals在Object中定义的原因。

如果一直这样做,你就不会有问题。

答案 13 :(得分: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));

答案 14 :(得分:0)

我认为灵感来自 Ray Hulha解决方案的最简单的解决方案是序列化对象,然后深入比较原始结果。

序列化可以是字节,json,xml或简单的toString等.ToString似乎更便宜。 Lombok为我们生成免费的易于定制的ToSTring。见下面的例子。

const MyObject = (() => {
  let id = 0;

  return function() {
    this.id = id++;
  };
})();

答案 15 :(得分:-1)

从Java 7开始,Objects.deepEquals(Object, Object)