1. 概述

在Java和其他许多编程语言中,比较对象是核心概念之一。在处理排序、搜索和过滤数据时,它扮演着至关重要的角色,对编程的各个方面都有着不可或缺的作用。

在Java中比较对象可以通过手动实现比较逻辑或使用具有对象比较能力的库来完成。Java中有多种库可用于对象比较,如JaVers或Apache Commons Lang 3,本文将对此进行介绍。

2. 关于Apache Commons Lang 3

Apache Commons Lang 3是Apache Commons Lang库的3.0版本,提供了众多功能

我们将探索org.apache.commons.lang3.builder.DiffBuilder类,用于比较同一类型的两个对象并获取它们之间的差异。差异结果由org.apache.commons.lang3.builder.DiffResult类表示。

此外,ReflectionDiffBuilder链接)是DiffBuilder的另一种选择,它们都服务于相同的目的,但ReflectionDiffBuilder基于反射,而DiffBuilder则不然。

3. Maven依赖

要使用Apache Commons Lang 3,首先需要添加Maven依赖:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.14.0</version>
</dependency>

4. 模型

为了演示比较两个对象及其差异,我们将使用一个Person类,以及PhoneNumberAddress类:

public class Person {
    private String firstName;
    private String lastName;
    private int age;
    private List<PhoneNumber> phoneNumbers;
    private Address address;

    // standard constructors, getters and setters
}
public class PhoneNumber {
    private String type;
    private String number;

    // standard constructors, getters and setters
}
public class Address {
    private String streetAddress;
    private String city;
    private String postalCode;

    // standard constructors, getters and setters
}

5. 使用DiffBuilder类比较对象

现在我们通过DiffBuilder类展示如何比较Person对象。首先,我们定义一个名为PersonDiffBuilder的类,并实现compare()方法:

public static DiffResult compare(Person first, Person second) {
    DiffBuilder diffBuilder = new DiffBuilder(first, second, ToStringStyle.DEFAULT_STYLE)
      .append("person", first.getFirstName(), second.getFirstName())
      .append("lastName", first.getLastName(), second.getLastName())
      .append("streetAddress", first.getAddress().getStreetAddress(), 
        second.getAddress().getStreetAddress())
      .append("city", first.getAddress().getCity(), second.getAddress().getCity())
      .append("postalCode", first.getAddress().getPostalCode(), 
        second.getAddress().getPostalCode())
      .append("age", first.getAge(), second.getAge());

    for (int i = 0; i < first.getPhoneNumbers().size(); i++) {
        diffBuilder.append("phoneNumbers[" + i + "].number", 
          first.getPhoneNumbers().get(i).getNumber(), 
          second.getPhoneNumbers().get(i).getNumber());
    }
    return diffBuilder.build();
}

在这里,我们使用DiffBuilder来实现compare()方法。当我们使用append()方法生成DiffBuilder时,可以精确控制哪些字段将参与比较。

在演示时,当我们比较嵌套的PhoneNumber对象时,我们会忽略type字段,因此拥有相同号码但类型不同的两个PhoneNumber对象将被视为相等。

如果愿意,可以让Person类实现Diffable接口,然后同样使用DiffBuilder来实现diff()方法。

让我们看一个实际应用PersonDiffBuilder类,比较两个Person对象的例子:

@Test
void givenTwoPeopleDifferent_whenComparingWithDiffBuilder_thenDifferencesFound() {
    List<PhoneNumber> phoneNumbers1 = new ArrayList<>();
    phoneNumbers1.add(new PhoneNumber("home", "123-456-7890"));
    phoneNumbers1.add(new PhoneNumber("work", "987-654-3210"));

    List<PhoneNumber> phoneNumbers2 = new ArrayList<>();
    phoneNumbers2.add(new PhoneNumber("mobile1", "123-456-7890"));
    phoneNumbers2.add(new PhoneNumber("mobile2", "987-654-3210"));

    Address address1 = new Address("123 Main St", "London", "12345");
    Address address2 = new Address("123 Main St", "Paris", "54321");

    Person person1 = new Person("John", "Doe", 30, phoneNumbers1, address1);
    Person person2 = new Person("Jane", "Smith", 28, phoneNumbers2, address2);

    DiffResult<Person> diff = PersonDiffBuilder.compare(person1, person2);
    for (Diff<?> d : diff.getDiffs()) {
        System.out.println(d.getFieldName() + ": " + d.getLeft() + " != " + d.getRight());
    }

    assertFalse(diff.getDiffs().isEmpty());
}

得到的DiffResult提供了getDiffs()方法,用于获取发现的差异作为Diff对象的列表。Diff类还提供了获取比较字段的实际方法。

在这个例子中,比较的人有不同的人名、姓氏、城市和邮政编码。电话号码类型不同但号码相同。

查看System.out.println()输出,我们可以看到所有差异已被找到:

person: John != Jane
lastName: Doe != Smith
city: London != Paris
postalCode: 12345 != 54321
age: 30 != 28

6. 使用ReflectionDiffBuilder类比较对象

接下来,我们演示如何使用ReflectionDiffBuilder类比较Person对象。首先,我们定义一个名为PersonReflectionDiffBuilder的类,并实现compare()方法:

public static DiffResult compare(Person first, Person second) {
    return new ReflectionDiffBuilder<>(first, second, ToStringStyle.SHORT_PREFIX_STYLE).build();
}

这里,我们使用ReflectionDiffBuilder来实现compare()方法。不需要为比较单独附加字段,因为所有非静态和非transient字段都会通过反射发现。

在本例中,发现的字段将是firstNamelastNameagephoneNumbersaddress。内部地,ReflectionDiffBuilder使用了DiffBuilder,并基于发现的字段构建。

如果我们想排除比较中的特定发现字段,可以在ReflectionDiffBuilder的使用上标记@DiffExclude注解,标记我们希望排除的字段。

由于我们的Person类有复杂的结构,包含嵌套的对象,为了确保ReflectionDiffBuilder正确识别差异,我们需要实现equals()hashCode()方法。

为了演示目的,我们将Person类的address字段标记为@DiffExclude注解:

public class Person {
    private String firstName;
    private String lastName;
    private int age;
    private List<PhoneNumber> phoneNumbers;
    @DiffExclude
    private Address address;

    // standard constructors, getters and setters
}

同时,我们将在PhoneNumber类的equals()hashCode()方法中忽略type字段:

@Override
public boolean equals(Object o) {
    if (this == o) {
        return true;
    }
    if (o == null || getClass() != o.getClass()) {
        return false;
    }
    PhoneNumber that = (PhoneNumber) o;
    return Objects.equals(number, that.number);
}

@Override
public int hashCode() {
    return Objects.hash(number);
}

让我们看如何使用PersonReflectionDiffBuilder类比较两个Person对象:

@Test
void givenTwoPeopleDifferent_whenComparingWithReflectionDiffBuilder_thenDifferencesFound() {
    List<PhoneNumber> phoneNumbers1 = new ArrayList<>();
    phoneNumbers1.add(new PhoneNumber("home", "123-456-7890"));
    phoneNumbers1.add(new PhoneNumber("work", "987-654-3210"));

    List<PhoneNumber> phoneNumbers2 = new ArrayList<>();
    phoneNumbers2.add(new PhoneNumber("mobile1", "123-456-7890"));
    phoneNumbers2.add(new PhoneNumber("mobile2", "987-654-3210"));

    Address address1 = new Address("123 Main St", "London", "12345");
    Address address2 = new Address("123 Main St", "Paris", "54321");

    Person person1 = new Person("John", "Doe", 30, phoneNumbers1, address1);
    Person person2 = new Person("Jane", "Smith", 28, phoneNumbers2, address2);

    DiffResult<Person> diff = PersonReflectionDiffBuilder.compare(person1, person2);
    for (Diff<?> d : diff.getDiffs()) {
        System.out.println(d.getFieldName() + ": " + d.getLeft() + " != " + d.getRight());
    }

    assertFalse(diff.getDiffs().isEmpty());
}

在这个例子中,比较的人有不同的名字、姓氏和地址。电话号码类型不同但号码相同。然而,我们使用@DiffExclude注解排除了address字段,不参与比较。

查看System.out.println()输出,我们可以看到所有差异已被找到:

firstName: John != Jane
lastName: Doe != Smith
age: 30 != 28

7. 总结

本文展示了如何使用Apache Commons Lang 3库中的DiffBuilderReflectionDiffBuilder类来比较Java对象。

这两个类易于使用,提供了方便的对象比较方式,尽管各自有优缺点。

通过本文中的示例,我们看到DiffBuilder提供了更多的定制选项,更明确。然而,对于更复杂的对象,这可能会增加复杂性。

ReflectionDiffBuilder提供了更大的简洁性,但定制选项有限,并可能引入性能开销,因为它使用了反射。

文章中的代码可以在GitHub上找到。