有没有一种Java工具可以深度比较两个对象?

105

如何在测试中比较两个没有实现equals方法的对象并基于它们的字段值进行“深度”比较?


原问题(因缺乏准确性而被关闭,为了记录目的而保留):

我正在尝试为大型项目内的各种clone()操作编写单元测试,并想知道是否有现有的类能够接受两个相同类型的对象,进行深度比较,并判断它们是否相同?


1
这个类如何知道在对象图的某个特定点它能够接受相同的对象,还是只能接受相同的引用? - Zed
3
我想说的是,你无论如何都需要配置(即实现)比较。那么,为什么不重写类中的equals方法并使用它呢? - Zed
3
如果对于一个大而复杂的对象,等式运算符返回了 false,你应该从哪里开始?最好将该对象转换为多行字符串并进行字符串比较。然后,您可以准确地看到两个对象的差异在哪里。IntelliJ 弹出一个“更改”比较窗口,帮助找到两个结果之间的多个更改,即它能够理解 assertEquals(string1, string2) 的输出并给出比较窗口。 - Peter Lawrey
这里有一些非常好的答案,除了被接受的那个答案之外,似乎已经被掩埋了。 - user1445967
问题的答案是“是”。除了使用DeepEquals之外,还有另一种选择。使用java-util(https://github.com/jdereg/java-util)的GraphComparator.compare()方法,它将在两个图形之间生成差异列表(Delta列表)。如果列表为空,则这些图形是等效的。如果列表中有项目,则这些项目是应用于源以使其等于目标图形的指令。GraphComparator.applyDelta(source, deltaList)将使源与目标图形匹配。它可以正确处理图形内的循环。 - John DeRegnaucourt
显示剩余2条评论
14个回答

64

Unitils拥有以下功能:

通过反射进行等式断言,提供不同选项,如忽略Java默认/ null值和忽略集合的顺序。


10
我已对此函数进行了一些测试,它似乎进行了深度比较,而EqualsBuilder则没有。 - Howard May
有没有办法让它不忽略瞬态字段? - Pinch
@Pinch 我明白你的意思。我认为unitils中的深度比较工具存在缺陷,因为它即使在变量没有可观察影响时也会进行比较。比较变量的另一个(不良)后果是不支持纯闭包(没有自己状态的闭包)。此外,它要求比较对象具有相同的运行时类型。我卷起袖子,创建了自己的版本的深度比较工具,解决了这些问题。 - beluchin
@Wolfgang有没有示例代码可以直接给我们呢?你引用的那句话是从哪里找到的? - fIwJlxSzApHEZIl

34

我很喜欢这个问题!主要是因为它几乎从未被解答或者解答得很差。就像没有人想出来一样。是处于未开垦领域 :)

首先,不要考虑使用equals方法。如javado中定义的那样,equals方法的契约是等价关系(自反、对称和传递),而不是相等关系。若是相等关系,就必须还具有反对称性质。唯一一个实现equals的方式,即使一个真正的相等关系,是在java.lang.Object中的。即使你使用equals来比较图中的所有内容,违反契约的风险仍然很高。如Josh Bloch所指出的在Effective Java中,equals的契约非常容易被破坏:

"根本没有办法扩展一个可实例化的类并添加一个方面同时保持equals合同"

此外,布尔方法到底有什么好处呢?实际上,将原始对象与克隆对象之间的所有区别封装起来也是不错的选择,您不认为吗?此外,在这里我假设您不想麻烦地编写/维护每个对象在图中的比较代码,而是想要找到一些随着源对象随时间变化而扩展的东西。

所以,您真正需要的是某种状态比较工具。该工具的实现方式取决于您的领域模型的性质和性能限制。根据我的经验,没有通用的魔力武器。对于大量迭代,它将会慢。但是对于测试克隆操作的完整性,它会做得相当好。您的两个最佳选项是序列化和反射。

您将遇到的一些问题:

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

XStream非常快速,并且与XMLUnit结合使用可以只需几行代码即可完成工作。 XMLUnit很好用,因为它可以报告所有差异,或者仅停在它发现的第一个差异处。并且其输出包括不同节点的xpath,这很好。默认情况下,它不允许无序集合,但可以配置为执行此操作。注入一个特殊的差异处理程序(称为)允许您指定处理差异的方式,包括忽略顺序。然而,一旦要做任何比最简单的自定义更复杂的事情,编写起来就变得困难,细节倾向于与特定领域对象相关联。

我个人喜欢使用反射循环遍历所有声明的字段,并深入每个字段,同时跟踪不同之处。警告:除非您喜欢堆栈溢出异常,否则不要使用递归。使用LinkedList或类似的东西让事


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

我认为这实际上是一个非常困难的问题,但完全可以解决!一旦你找到适合自己的方法,它就非常非常方便 :)

因此,祝你好运。如果你想出了什么绝妙的方法,请不要忘记分享!


20

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

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

可能并非所有情况都适用,但它可以适用于比你想象的更多的情况。

以下是文档中的说明:

断言被测试的对象(实际值)基于递归的属性/字段比较(包括继承的属性/字段),与给定对象相等。如果实际值的equals实现不适合您,则这可能很有用。对具有自定义equals实现的字段不应用递归属性/字段比较,即将使用重写的equals方法而不是逐个字段比较。

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

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

要比较的对象可以是不同类型的,但必须具有相同的属性/字段。例如,如果实际对象有一个名为name的字符串字段,则希望其他对象也有一个该字段。如果一个对象有一个字段和一个具有相同名称的属性,则属性值将优先于字段。


6
isEqualToComparingFieldByFieldRecursively е·Іиў«ејѓз”ЁпјЊиЇ·дЅїз”Ё assertThat(expectedObject).usingRecursiveComparison().isEqualTo(actualObject); д»Јж›ї :) - dargmuesli

15

覆盖equals()方法

你可以使用EqualsBuilder.reflectionEquals()来简单地覆盖类的equals()方法,具体请参考这里:

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

这个不起作用。 - fuat
1
它确实有效,并且如果您想进行微调,它有很多配置选项。但是它确实依赖于链中更远的对象具有类似的等于方法。 - Matthew Read

8

我刚刚需要实现Hibernate Envers修订的两个实体实例之间的比较。我开始编写自己的区别,但后来发现了以下框架。

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

你可以比较两个相同类型的对象,它会显示出改动、添加和移除。如果没有改动,则对象相等(理论上)。在检查中提供了应该忽略的getter的注释。该框架的应用远不止于相等性检查,例如我正在使用它生成变更日志。

对于比较JPA实体,它的性能还可以,但请确保首先将它们从实体管理器中分离。


6

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
特别是在处理生成的类时非常有用,因为您对equals没有任何影响! - Matthias B
1
https://dev59.com/n3M_5IYBdhLWcg3wRw51#1449051 已经提到了这一点。你应该编辑那篇帖子。 - user829755
1
@user829755 这样我就会失去积分。所以这都是关于积分游戏的)) 人们喜欢因完成工作而获得信用,我也一样。 - gavenkoa

5

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

}

这很好用,直到你有一个使用isFoo读取方法来读取Boolean属性的POJO。自2016年以来一直有一个PR来修复它。https://github.com/hamcrest/JavaHamcrest/pull/136 - Snekse

4

我正在使用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
除了列表以外的集合可能以不同的顺序返回元素,因此字符串比较会失败。 - Alexey Berezkin
不可序列化的类也会失败。 - Zwelch

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
我认为受 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());
    }
}   

尝试了许多不同的类和库来比较bean之后,我最终采取了这种方法。如果有良好的toString()实现,这将非常有效。然而,我仍希望有一种标准化的方式。 - Avec

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接