Comparator.comparing(...).thenComparing(...) find out which fields did not match

Viewed 1199

I am trying to compare two objects of same class and the goal is to compare them as well as identify which fields didn't match.

Example of my domain class

@Builder(toBuilder=true)
class Employee {
     String name;
     int age;
     boolean fullTimeEmployee;
}

Two objects

Employee emp1 = Employee.builder().name("john").age(25).fullTime(false).build();
Employee emp2 = Employee.builder().name("Doe").age(25).fullTime(true).build();

Comparing both objects

int result = Comparator.comparing(Employee::getName, Comparator.nullsFirst(Comparator.naturalOrder()))
                       .thenComparing(Employee::getAge, Comparator.nullsFirst(Comparator.naturalOrder()))
                       .thenComparing(Employee::isFullTimeEmployee, Comparator.nullsFirst(Comparator.naturalOrder()))
                       .compare(emp1, emp2);

result will be 0 because name & fullTime fields are not matching with each other.

But I also want to produce a list of fields which didn't match.. like below

List<String> unmatchedFields = ["name","fulltimeEmployee"];

Can I do it in a nicer way, other than bunch of if() else

3 Answers

First of all, I don't think you understand how comparing().thenComparing() works. The result in your case won't be 0. The comparator compares the first statement, if they are equal it goes to the thenComparing() and so on. The result will be 0 if and only if all comparison()/thenComparing() are also equal. I actually wrote about this a few days ago: http://petrepopescu.tech/2021/01/simple-collection-manipulation-in-java-using-lambdas/

Anyway, back to your question, I think you are looking for an equal() where it also returns what fields were not equal. Here is a quick prototype.

public class Pair<T, U> {
    private Function<? super T, ? extends Comparable> function;
    private String fieldName;

    public Pair(Function<? super T, ? extends Comparable> function, String fieldName) {
        this.function = function;
        this.fieldName = fieldName;
    }
}

And the actual comparator:

public class MyComparator {
    List<Pair> whatToCompare = Arrays.asList(
            new Pair<Employee, String>(Employee::getName, "name"),
            new Pair<Employee, String>(Employee::getAge, "age"),
            new Pair<Employee, Double>(Employee::isFullTimeEmployee, "fullTimeEmployee")
    );
    public List<String> compare(Employee e1, Employee e2) {
        List<String> mismatch = new ArrayList<>();
        for(Pair pair:whatToCompare) {
            int result = Comparator.comparing(pair.getFunction()).compare(e1, e2);
            if (result != 0) {
                mismatch.add(pair.getFieldName());
            }
        }

        return mismatch;
    }
}

Check out DiffBuilder. It can report which items are different.

DiffResult<Employee> diff = new DiffBuilder(emp1, emp2, ToStringStyle.SHORT_PREFIX_STYLE)
       .append("name", emp1.getName(), emp2.getName())
       .append("age", emp1.getAge(), emp2.getAge())
       .append("fulltime", emp1.getFulltime(), emp2.getFulltime())
       .build();

DiffResult has, among other things, a getDiffs() method that you can loop over to find what differs.

Comparator itself has no ability to discover out the differences, it only compares two objects of the same type and returns -1, 0 or 1. This is the interface contract.

To discover the actual differences, you need to use Reflection API to get the fields, compare the equality of their real values of two passed objects and let the differences be listed. Here is a generic implementation:

public class Difference<T> {

    T left;
    T right;

    //all-args constructor omitted

    public Set<String> differences() throws IllegalAccessException {

        // fieldName-field as a key-value  for the left and right instances
        Map<String, Field> leftFields = Arrays.stream(left.getClass().getDeclaredFields())
            .peek(f -> f.setAccessible(true))
            .collect(Collectors.toMap(Field::getName, Function.identity()));
        Map<String, Field> rightFields = Arrays.stream(left.getClass().getDeclaredFields())
            .peek(f -> f.setAccessible(true))
            .collect(Collectors.toMap(Field::getName, Function.identity()));

        // initialize Set as long as two fields cannot have a same name
        Set<String> differences = new HashSet<>();

        // list the fields of the left instance
        for (Entry<String, Field> entry: leftFields.entrySet()) {
            String fieldName = entry.getKey();
            Field leftField = entry.getValue();
            // find the right instance field matching the name
            Field rightField = rightFields.get(fieldName);
            // compare their actual values and add among the differences if they aren't equal
            if (!leftField.get(left).equals(rightField.get(right))) {
                differences.add(fieldName);
            }
        }
        return differences;
    }
}
Employee emp1 = Employee.builder().name("john").age(25).fullTime(false).build();
Employee emp2 = Employee.builder().name("Doe").age(25).fullTime(true).build();
        
Set<String> differences = new Difference<Employee>(emp1, emp2).differences();
        
System.out.println(differences);

[name, fullTimeEmployee]

Related