【问题标题】:Is there a Java utility to do a deep comparison of two objects? [closed]是否有 Java 实用程序可以对两个对象进行深入比较? [关闭]
【发布时间】:2010-11-29 18:11:53
【问题描述】:

如何在测试中根据字段值“深入”比较两个未实现 equals 方法的对象?


原始问题(因缺乏精确性而关闭,因此不符合 SO 标准),保留用于文档目的:

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

【问题讨论】:

  • 这个类如何知道在对象图的某个点它是否可以接受相同的对象,或者只能接受相同的引用?
  • 理想情况下,它会足够可配置:) 我正在寻找自动的东西,以便如果添加(而不是克隆)新字段,测试可以识别它们。
  • 我想说的是,无论如何您都需要配置(即实施)比较。那么为什么不在你的类中重写 equals 方法并使用它呢?
  • 如果 equals 对大型复杂对象返回 false,你从哪里开始?最好将对象转换为多行字符串并进行字符串比较。然后你可以准确地看到两个对象不同的地方。 IntelliJ 会弹出一个“更改”比较窗口,帮助查找两个结果之间的多个更改,即它理解 assertEquals(string1, string2) 的输出并为您提供一个比较窗口。
  • 这里有一些非常好的答案,除了公认的,似乎已经被埋没了

标签: java comparison equals


【解决方案1】:

AssertJ,你可以这样做:

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

它可能不会在所有情况下都有效,但它会在您认为的更多情况下有效。

文档是这样说的:

断言被测对象(实际)等于给定的 基于逐个属性/字段递归属性/字段的对象 比较(包括继承的)。如果实际情况,这可能很有用 equals 实现不适合你。递归属性/字段 比较不适用于具有自定义等于的字段 实现,即将使用覆盖的 equals 方法 逐个字段比较。

递归比较处理循环。默认情况下,浮动是 与 1.0E-6 的精度相比,是 1.0E-15 的两倍。

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

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

【讨论】:

  • isEqualToComparingFieldByFieldRecursively 现在已弃用。请改用assertThat(expectedObject).usingRecursiveComparison().isEqualTo(actualObject); :)
【解决方案2】:

重写equals()方法

您可以使用 EqualsBuilder.reflectionEquals() 简单地覆盖类的 equals() 方法,如 here 所述:

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

【讨论】:

  • 这不起作用
【解决方案3】:

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

【讨论】:

    【解决方案4】:

    我认为受 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() 实现,效果很好。不过,我确实希望有一种标准化的方式。
    【解决方案5】:

    如此深度比较的暂停保证可能是个问题。下面应该怎么做? (如果你实现了这样一个比较器,这将是一个很好的单元测试。)

    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:不,这不是一个新问题。答案中的问号不会使该标志合适。请多加注意。
    • 来自:Using an answer instead of a comment to get a longer limit and better formatting. 如果这是评论,为什么要使用答案部分?这就是我标记它的原因。不是因为?。这个答案已经被其他人标记了,他们没有留下评论。我刚刚在审核队列中得到了这个。可能是我的错,我应该更加小心。
    【解决方案6】:

    如果你的对象实现了 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);
        }
    }
    

    【讨论】:

      【解决方案7】:

      Hamcrest 有 Matcher 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;
          }
      
      }
      

      【讨论】:

      【解决方案8】:

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

      【讨论】:

      • 特别有用,如果你必须处理生成的类,你对 equals 没有任何影响!
      • stackoverflow.com/a/1449051/829755 已经提到了这一点。你应该编辑过那个帖子
      • @user829755 这样我就丢分了。所以关于点游戏))人们喜欢通过完成工作获得学分,我也是。
      【解决方案9】:

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

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

      【讨论】:

      • 警告:DeepEquals 使用对象的 .equals() 方法(如果存在)。这可能不是你想要的。
      • 如果明确添加了 equals() 方法,它只会在类上使用 .equals(),否则会进行逐个成员的比较。这里的逻辑是,如果有人努力编写自定义的 equals() 方法,那么应该使用它。未来的增强:允许标志让它忽略 equals() 方法,即使它们存在。 java-util 中有一些有用的实用程序,例如 CaseInsensitiveMap / Set。
      • 我担心比较字段。从对象客户端的角度来看,字段的差异可能无法观察到,但基于字段的深度比较仍然会标记它。此外,比较字段要求对象具有相同的运行时类型,这可能会受到限制。
      • 回答上面的@beluchin,DeepEquals.deepEquals() 并不总是进行逐个字段的比较。首先,它可以选择在一个方法上使用 .equals() 如果存在一个方法(即不是 Object 上的那个),或者它可以被忽略。其次,比较 Maps/Collections 时,不看 Collection 或 Map 类型,也不看 Collection/Map 上的字段。相反,它在逻辑上比较它们。如果 LinkedHashMap 具有相同的内容和元素以相同的顺序,则它们可以等于 TreeMap。对于无序的集合和地图,只需要 size 和 deep-equals 项。
      • 在比较 Maps/Collections 时,它不会查看 Collection 或 Map 类型,也不会查看 Collection/Map 上的字段。相反,它在逻辑上比较它们 @JohnDeRegnaucourt,我认为这种逻辑比较,即仅比较public 的内容应该适用于所有类型,而不是仅适用于集合/地图。
      【解决方案10】:

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

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

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

      它的性能还可以,在比较JPA实体时,一定要先将它们从实体管理器中分离出来。

      【讨论】:

        【解决方案11】:

        您的链接列表示例并不难处理。当代码遍历两个对象图时,它将访问过的对象放置在 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() 项目。我还没有它的名字,但它会很明显。

        【讨论】:

          【解决方案12】:

          我喜欢这个问题!主要是因为它几乎没有被回答或回答得不好。好像还没有人想通。处女地:)

          首先,不要考虑使用equalsequals 的契约,如 javadoc 中所定义,是等价关系(自反、对称和传递),不是等价关系。为此,它还必须是反对称的。 equals 的唯一实现是(或永远可能是)真正的相等关系是java.lang.Object 中的实现。即使您确实使用equals 来比较图表中的所有内容,违约的风险也相当高。正如 Josh Bloch 在 Effective Java 中指出的那样,equals 的契约很容易被打破:

          “根本没有办法扩展可实例化的类并添加方面,同时保留 equals 协定”

          除了布尔方法对你有什​​么好处吗?将原版和克隆版之间的所有差异真正封装起来会很好,你不觉得吗?另外,我在这里假设您不想为图表中的每个对象编写/维护比较代码而烦恼,而是您正在寻找可以随着源代码随时间变化而扩展的东西。

          太好了,您真正想要的是某种状态比较工具。该工具的实现方式实际上取决于您的域模型的性质和您的性能限制。根据我的经验,没有通用的灵丹妙药。而且它在大量迭代中变慢。但是为了测试克隆操作的完整性,它会很好地完成这项工作。您的两个最佳选择是序列化和反射。

          你会遇到的一些问题:

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

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

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

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

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

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

          【讨论】:

            【解决方案13】:

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

            这种事情就是为什么在Object中定义.equals。

            如果始终如一地这样做,您将不会有问题。

            【讨论】:

            • 问题是我想针对我没有编写的大型现有代码库自动测试它...... :)
            【解决方案14】:

            我正在使用 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();
            }
            

            【讨论】:

            • 列表以外的集合可能会以不同的顺序返回元素,因此字符串比较会失败。
            • 不可序列化的类也会失败
            【解决方案15】:

            Unitils 有这个功能:

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

            【讨论】:

            • 我已经对这个函数做了一些测试,它似乎做了一个深入的比较,而 EqualsBuilder 没有。
            • 有没有办法让它不忽略瞬态字段?
            • @Pinch 我听到了。我想说unitils 中的深度比较工具存在缺陷,因为它会比较变量,即使它们可能没有可观察的影响。比较变量的另一个(不希望的)后果是不支持纯闭包(没有自己的状态)。另外,它要求比较对象具有相同的运行时类型。我卷起袖子,创建了我的 own version 的深度比较工具来解决这些问题。
            • @Wolfgang 是否有任何示例代码可以指导我们?你是从哪里引用的?
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2018-02-28
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多