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


99

我正在尝试为clone()大型项目中的各种操作编写单元测试,并且我想知道某个地方是否存在一个现有的类,该类能够接受两个相同类型的对象,进行深度比较,并说是否是否相同?


1
此类如何知道在对象图的某个点上它可以接受相同的对象还是只能接受相同的引用?
Zed

理想情况下,它将是足够可配置的:)我正在寻找自动的东西,以便如果添加(而不是克隆)新字段,则测试可以识别它们。
Uri

3
我要说的是,无论如何,您都将需要配置(即实施)比较。那么,为什么不覆盖类中的equals方法并使用它呢?
Zed

3
对于大型复杂对象,如果equals返回false,那么从哪里开始呢?将对象转换为多行String并进行String比较会更好。然后,您可以确切看到两个对象的不同之处。IntelliJ弹出一个“ changes”比较窗口,该窗口可帮助您找到两个结果之间的多个变化,即它可以理解assertEquals(string1,string2)的输出并为您提供一个比较窗口。
彼得·劳瑞

除了被接受的答案以外,这里还有一些非常好的答案,似乎已经被
掩盖了

Answers:


62

Unitils具有以下功能:

通过反射进行平等声明,并具有不同的选项,例如忽略Java默认值/空值和忽略集合的顺序


9
我对此功能进行了一些测试,它似乎进行了深层比较,而EqualsBuilder却没有。
霍华德

有没有办法让它不忽略瞬态场?

@Pinch我听到你了。我要说的是,深度比较工具unitils存在缺陷,因为它即使在变量没有明显影响的情况下也可以比较变量。比较变量的另一个(不希望的)结果是不支持纯闭包(没有闭包状态)。另外,它要求比较的对象具有相同的运行时类型。我袖手旁观,并创建了自己的深度比较工具版本,以解决这些问题。
beluchin '16

@Wolfgang有任何示例代码可将我们定向到吗?你从哪儿来的那句话?
anon58192932'7

30

我喜欢这个问题!主要是因为几乎从未回答过或回答得很差。就像没有人知道。维尔京领土:)

首先,甚至不要考虑使用equals。如equalsjavadoc中所定义,的契约是等价关系(自反,对称和可传递),而不是等价关系。为此,它也必须是反对称的。唯一的实现equals是(或曾经是)真正的平等关系是中的java.lang.Object。即使您确实使用equals过比较图表中的所有内容,但违反合同的风险仍然很高。正如Josh Bloch在Effective Java中指出的那样,等于的约定很容易打破:

“根本没有办法扩展可实例化的类并在保留平等合同的同时增加一个方面”

除了布尔方法到底有什么用呢?实际上封装原始副本和克隆副本之间的所有差异会很好,您不觉得吗?另外,我在这里假设您不想为图表中的每个对象编写/维护比较代码而感到烦恼,而是在寻找随源随着时间变化而变化的东西。

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

您将遇到的一些问题:

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

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

我个人的喜好是使用反射来循环遍历所有声明的字段,并深入研究每个字段,并在进行过程中跟踪差异。警告词:除非您喜欢堆栈溢出异常,否则请不要使用递归。使用堆栈将内容保持在范围内(使用LinkedList或者其他的东西)。我通常会忽略瞬态和静态字段,并且会跳过已经比较过的对象对,因此,如果有人决定编写自引用代码,我就不会陷入无限循环(但是无论如何我都会比较原始包装器,因为通常会重复使用相同的对象ref)。您可以预先配置内容,以忽略集合的排序并忽略特殊类型或字段,但是我想通过注释在字段本身上定义状态比较策略。恕我直言,这正是注释的含义,它使有关类的元数据在运行时可用。就像是:


@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()方法(如果存在)。这可能不是您想要的。
亚当

4
如果显式添加了equals()方法,则仅在类上使用.equals(),否则将进行逐成员比较。这里的逻辑是,如果有人努力编写自定义的equals()方法,则应使用它。将来的增强:允许标志使它忽略equals()方法,即使它们存在也是如此。java-util中有有用的实用程序,例如CaseInsensitiveMap / Set。
John DeRegnaucourt 2014年

我担心比较字段。从对象客户端的角度来看,字段中的差异可能无法观察到,但基于字段的深度比较仍会标记出来。另外,比较字段要求对象具有相同的运行时类型,这可能会受到限制。
beluchin '16

要回答上面的@beluchin,DeepEquals.deepEquals()并不总是进行逐字段比较。首先,它可以选择在方法中使用.equals()(如果存在)(而不是Object上的方法),或者可以忽略该方法。其次,在比较地图/集合时,它不会查看集合或地图类型,也不会查看集合/地图上的字段。相反,它在逻辑上比较它们。如果LinkedHashMap的内容和元素具有相同的顺序,则它们可以等于TreeMap。对于无序的“收藏”和“地图”,仅需要大小和深度等于项。
John DeRegnaucourt

比较“地图/收藏夹”时,它不会查看“收藏夹”或“地图”类型,也不会查看“收藏夹/地图”上的字段。相反,它在逻辑上它们进行了 @JohnDeRegnaucourt的比较,我认为这种逻辑上的比较即将仅public适用于所有类型的内容进行比较,而不是仅适用于集合/映射。
贝卢钦

10

重写equals()方法

您可以使用EqualsBuilder.reflectionEquals()简单地覆盖类的equals()方法。作为解释在这里

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

7

只需比较Hibernate Envers修订的两个实体实例即可。我开始写自己的不同文章,但随后找到了以下框架。

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

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

它的性能还可以,在比较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'4

同样,无法序列化的类也会失败
Zwelch

6

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

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

可能并非在所有情况下都可以使用,但是在您认为更多的情况下都可以使用。

文档说明如下:

根据属性/字段比较(包括继承的对象),通过递归属性/字段来确定被测对象(实际)等于给定对象。如果实际的equals实现不适合您,这将很有用。递归属性/字段比较不适用于具有自定义equals实现的字段,即,将使用覆盖的equals方法代替逐字段比较的字段。

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

您可以为每个(嵌套的)字段或类型指定自定义比较器,分别使用ComparatorForFields(Comparator,String ...)和usingComparatorForType(Comparator,Class)。

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


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已经提到了这一点。您应该已经编辑了该帖子
user829755 2014年

1
@ user829755这样我就失去了积分。因此,所有关于点子游戏的知识都来了))人们也喜欢为完成的工作而获得荣誉,我也是。
gotnkoa 2014年

3

Hamcrest具有Matcher samePropertyValuesAs。但是它依赖于JavaBeans Convention(使用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;
    }

}

除非您有一个isFooBoolean属性使用read方法的POJO,否则这将非常有用。自2016年以来已经有一个PR进行修复。 github.com/hamcrest/JavaHamcrest/pull/136
Snekse

2

如果您的对象实现可序列化,则可以使用以下方法:

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

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

我同意上面的人所说的使用LinkedList(就像一个Stack,但是上面没有同步方法,所以它更快)。理想的解决方案是使用堆栈遍历对象图,同时使用反射来获取每个字段。一次编写完之后,所有的equals()和hashCode()方法都应调用此“外部” equals()和“外部” hashCode()。您再也不需要客户的equals()方法。

我写了一些遍历完整对象图的代码,列在Google Code中。请参阅json-io(http://code.google.com/p/json-io/)。它将Java对象图序列化为JSON并从中反序列化。它处理所有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

我猜您知道这一点,但是从理论上讲,您应该始终重写.equals来断言两个对象确实相等。这意味着他们检查其成员上重写的.equals方法。

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

如果始终如一地做到这一点,您将不会有任何问题。


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 2015年

从此:Using an answer instead of a comment to get a longer limit and better formatting.如果这是评论,那么为什么要使用答案部分?这就是为什么我标记它。不是因为?。答案已经被其他人标记,他们没有留下任何评论。我刚在审查队列中得到这个。可能对我不利,我应该更加小心。
YoungHobbit

0

我认为受Ray Hulha解决方案启发的最简单的解决方案是序列化对象,然后对原始结果进行深入比较。

序列化可以是字节,json,xml或简单的toString等。ToString似乎更便宜。龙目岛为我们生成免费的易于定制的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());
    }
}   

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.