深度反射比较相等。

23
我正在尝试通过比较生成的对象与原始对象来验证序列化和反序列化程序。这些程序可以序列化任意嵌套类,因此我需要一个比较程序,该程序可以接收原始实例和最终实例,并在反射式地遍历每个值类型并比较值的同时,迭代地深入到引用类型以比较值。
我已经尝试过Apache Commons Lang的EqualsBuilder.reflectionEquals(inst1, inst2),但它似乎没有进行非常深入的比较,而只是比较引用类型是否相等,而不进一步深入比较它们:
以下代码说明了我的问题。第一次调用reflectionEquals返回true,但第二次返回false。
有没有人能推荐一个库程序?
class dummy {
    dummy2 nestedClass;
}

class dummy2 {
    int intVal;
}

@Test
public void testRefEqu() {

    dummy inst1 = new dummy();
    inst1.nestedClass = new dummy2();
    inst1.nestedClass.intVal = 2;
    dummy inst2 = new dummy();
    inst2.nestedClass = new dummy2();
    inst2.nestedClass.intVal = 2;
    boolean isEqual = EqualsBuilder.reflectionEquals(inst1.nestedClass, inst2.nestedClass);
    isEqual = EqualsBuilder.reflectionEquals(inst1, inst2);
}

如果反射相等仅仅是比较引用,那么它就有缺陷了。它应该做更多的事情。 - DwB
@DwB 我怀疑代码的意图是允许您在特定类中反射实现equals()。这与我想要的不同,我想要反射两个对象实例。在这种情况下,这不是一个错误,而是一种失望! - Howard May
1
我因EqualsBuilder的这种弱不经风的行为浪费了半天时间。如果传递对象的字段是非原始类型,那么构建器只会调用object.equals()。非常令人失望和无用。 - AlexWien
4
@AlexWien 它远非无用。事实上,如果没有正确使用,完全递归的相等方法可能会非常危险!但是,我同意文档应该更清晰明了。我已经提出了https://issues.apache.org/jira/browse/LANG-1034来解决缺少的功能。我将提出另一个错误来解决误导性文件。 - Duncan Jones
1
@AlexWien 我从未见过任何名为equals的方法进行深度比较。深度比较似乎是一个很少需要的例外情况,所以您不应该对标准行为感到失望。 - maaartinus
@maartinus 一个(自定义的)equals方法必须检查对象是否完全相等:根据用例,自定义的equals必须检查所有嵌套对象:想象一个具有两个点对象的线对象。(当定义线的两个点相等时,该线是相等的=需要嵌套相等)。如果由于某些明确的原因自动equals是危险的,则至少必须进行文档记录。(感谢Duncan) - AlexWien
5个回答

16

根据这个问题的答案https://dev59.com/n3M_5IYBdhLWcg3wRw51#1449051,以及一些初步测试结果,看起来Unitils的ReflectionAssert.assertReflectionEquals会做你期望的事情。(编辑:但可能已经被遗弃,所以你可以尝试AssertJ https://assertj.github.io/doc/#assertj-core-recursive-comparison

2021年编辑:EqualsBuilder现在有一个testRecursive选项。然而,根据上下文,单元测试库会给出更好的错误信息来帮助调试,所以根据情况,它们仍然是最好的选择。


我分享你的惊讶,但公平地说,正如我在问题中评论的那样,我认为这是误解程序意图的问题。也许这是可以更明确的事情。 - Howard May
我已经对assertReflectionEquals进行了一些初步测试,它似乎可以正常工作。非常感谢。 - Howard May
1
@artbristol 我已经提出了 https://issues.apache.org/jira/browse/LANG-1035 来解决这个问题。我会尽力确保它在下一个版本中得到修复。 - Duncan Jones
Unitils似乎不再受支持(SO)。我改用AssertJ的 逐字段比较 - paulcm
开发人员需要这个。感谢提供的替代方案 =) 在 Rust 中,您只需在您的“struct”上键入#[derive(PartialEq)],然后您可以在任何地方测试深度相等性。它被广泛且成功地用于测试目的。Java 也肯定会受益于使用标准反射方法。 - nuiun

4

有一种方法是使用反射来比较对象-但这很棘手。另一种策略是比较序列化对象的字节数组:

class dummy implements Serializable {
    dummy2 nestedClass;
}

class dummy2  implements Serializable {
    int intVal;
}

@Test
public void testRefEqu() throws IOException {

    dummy inst1 = new dummy();
    inst1.nestedClass = new dummy2();
    inst1.nestedClass.intVal = 2;

    dummy inst2 = new dummy();
    inst2.nestedClass = new dummy2();
    inst2.nestedClass.intVal = 2;

    boolean isEqual1 = EqualsBuilder.reflectionEquals(inst1.nestedClass, inst2.nestedClass);
    boolean isEqual2 = EqualsBuilder.reflectionEquals(inst1, inst2);

    System.out.println(isEqual1);
    System.out. println(isEqual2);

    ByteArrayOutputStream baos1 =new ByteArrayOutputStream();
    ObjectOutputStream oos1 = new ObjectOutputStream(baos1);
    oos1.writeObject(inst1);
    oos1.close();

    ByteArrayOutputStream baos2 =new ByteArrayOutputStream();
    ObjectOutputStream oos2 = new ObjectOutputStream(baos2);
    oos2.writeObject(inst1);
    oos2.close();

    byte[] arr1 = baos1.toByteArray();
    byte[] arr2 = baos2.toByteArray();

    boolean isEqual3 = Arrays.equals(arr1, arr2);

    System.out.println(isEqual3);

}

您的应用程序需要对对象进行序列化和反序列化,因此这种方法似乎是解决问题的最快方法(从CPU操作的角度来看)。


谢谢,Johnny。我曾考虑过比较序列化形式,这是一个巧妙的方法,可以解决EqualsBuilder的缺点。但是,它并不能完全验证序列化和反序列化,因为它并没有确认原始未序列化形式与反序列化形式相同。祝好。 - Howard May
嗨霍华德,你能给个例子说明这种情况可能发生吗?我确定一个对象只有一个序列化形式和字节表示。 - whysoserious
嗨Johnny,记得这样做是为了测试我的错误代码。如果我的序列化器无法序列化特定字段,则比较序列化版本将无法检测到问题。 - Howard May
@HowardMay 我和你处于同样的情况。这个答案让我想起了我很久以前做过的事情。这种序列化测试方法是可行的,因为Java标准序列化是有效的。(否则你会得到一个序列化异常,或者你已经覆盖了Java序列化)。 - AlexWien

3

你可以使用AssertJ的逐字段递归比较功能,例如:

import static org.assertj.core.api.BDDAssertions.then;

then(actualObject).usingRecursiveComparison().isEqualTo(expectedObject);

0

在相关的类中实现equals()方法。每个调用的equals将比较嵌套类的相等性(或者,如果您愿意,将比较数据成员的相等性)。正确编写的equals方法将始终导致深度比较。

在您的示例中,dummy类的equals应该像这样:

public boolean equals(Object other)
{
    if (other == this) return true;
    if (other instanceOf dummy)
    {
        dummy dummyOther = (dummy)other;
        if (nestedClass == dummyOther.nestedClass)
        {
           return true;
        }
        else if (nestedClass != null)
        {
           return nestedClass.equals(dummyOther);
        }
        else // nestedClass == null and dummyOther.nestedClass != null.
        {
           return false;
        }
    }
    else
    {
      return false;
    }
}

3
我理解这是实现此目的的正常方式,并且在大多数情况下是推荐的方式。内置的计算相等性的机制是强大的且可扩展的,以允许自定义类定义它们的相等性含义。不幸的是,我的要求阻止我能够在所有嵌套类上实现equals(),因此我希望使用反射。谢谢。 - Howard May
1
这种方法的问题在于无法处理循环对象图。使用这种方法比较循环对象图会导致无限递归。 - jhegedus
我同意循环对象图会是一个问题,就像序列化和故障转移一样(这可能只是序列化的症状)。 - DwB

0

我知道这是一个晚回复,但这可能对需要帮助的人有用。在我的项目中,我需要比较整个反序列化值集以查看它们是否匹配,我使用的依赖项是Javers。它与SpringBoot集成良好,使用非常简单。

这是示例实现:

@RunWith(SpringRunner.class)
public class DeserializerTest {

    private HubMessage message;
    private static final String EXPECTED_INVALID_MSG = "testInvalidMessage";
    private PlatformEventHeader msgHeaders;
    private ObjectMapper objectMapper = new ObjectMapper();
    private final DeserializerClass testDeserializerClass = new DeserializerClass();
    private String TOPIC_TEST = "testTopic";
    private static final String ACTUAL_HUBMESSAGE = "XXXStringValueofmessage";
    @Before
    public void stubSetup() throws IOException {
        // setup the message
       message = new HubMessage ();

        //setup the headers
       msgHeaders = new PlatformEventHeader();
        ...
        ...

        DomainEvent event= new DomainEvent();
        Map<String, String> msgEventObjMap = new HashMap<>();
        ...
        event.setMessage(msgEventObjMap);
        message.setEvent(event);
        message.setHeader(msgHeaders);

    }

    @Test
   public void test_deserialize_validMessage() throws JsonProcessingException {
          // My actual deserialzierClass
          HubMessage messageReceived = testDeserializerClass.deserialize(TOPIC_TEST,  
             new RecordHeaders(),ACTUAL_HUBMESSAGE .getBytes());

        Javers javers = JaversBuilder.javers().build();
        Diff diff = javers.compare((Object)objectMapper.convertValue(messageReceived, Object.class),
                (Object)objectMapper.readValue(ACTUAL_HUBMESSAGE, Object.class));
        Assert.assertEquals(diff.getChanges().size() , 0);
    }

我使用Diff来确定对象是否有任何更改。现在覆盖/使用toString()不是有效的方法,因为它期望匹配器中预期值和实际值的属性顺序相同。Unitils解决这个问题可能会带来很多麻烦,因为会下载很多第三方jar包。所以更好的选择是使用Javers.io

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