Comparator building logic using Java 8 for any number of fields in class

Viewed 163

I am trying to build custom comparison logic using Comparator.comparing() introduced in Java 8.

Requirement is: code should work if a new field is introduced in Person class. It should take new field into comparison logic.

It means comparison logic should be like this : id --> name --> new field1 --> new field2

See pseudo code below :

package test;

import java.lang.reflect.Field;
import java.util.Comparator;

public class Test6 {
    
    public static void main (String args []) {
        Field[] declaredFields = Person.class.getDeclaredFields();
        
        Comparator<Person> comparator = Comparator.comparing((Person x)-> x.getId());
        for (int i=1; i<declaredFields.length ; i++) {
            comparator = comparator.thenComparing(field[i]);  // what code should I place over here, to make it work for field obtained above.
        }

   List<Person> persons = new ArrayList<>();
     persons.stream().sorted(comparator).collect(Collectors.toList());
    }


}

class Person{
    int id ;
    String name;
    // if we add new field , then this field should also be included in comparison logic

    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    
    
    
}
3 Answers

In Apache Commons BeanUtils there is a useful comparator BeanComparator based on Reflection. Combining with ReverseComparator it's possible to write a dynamic comparator.

<dependency>
   <groupId>commons-beanutils</groupId>
   <artifactId>commons-beanutils</artifactId>
   <version>1.9.3</version>
</dependency>

Simple example:

public static <T> Comparator<T> beanComparator(String fieldName, boolean asc) {
    Comparator<T> propertyComparator;
    if (asc) {
        propertyComparator = new BeanComparator<>(fieldName);
    } else {
        propertyComparator = new ReverseComparator(new BeanComparator<>(fieldName));
    }
    return propertyComparator;
}

To do what your trying to do is problematic since you need to pass a function (which would describe a return type) to a comparator using reflection. Since I presume you would need to modify the class anyway to contain the new fields as well as their getters and the constructor. And by doing it this way you specify the order in which the getters are applied in the comparisons.

class Person implements Comparable<Person> {
    int id;
    String name;
    String foo;

    private Comparator<Person> comparing = Comparator.comparing(Person::getId);
  
    {
        comparing = comparing.thenComparing(Person::getName);
        comparing = comparing.thenComparing(Person::getFoo);
        // as new fields/getters are added, also append (or insert) the 
        // comparator.
    }
    
    public int getId() {
        return id;
    }
    
    public void setId(int id) {
        this.id = id;
    }
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    public int compareTo(Person p) {
        return comparing.compare(this, p);
    }
    
    public String getFoo() {
        return foo;
    }
    
}

Then you can call it like this.

List<Person> sortedPersons = persons.stream().sorted()
        .collect(Collectors.toList());

Or if they are already in a List

persons.sort(null); // null == natural ordering.

You can use the Field.get method, which returns the value of that field on the object passed as an argument. get throws a checked exception, so we have to catch it and re-throw as an unchecked exception.

Field[] fields = Person.class.getDeclaredFields();

Comparator<Person> comparator = Comparator.comparing(Person::getId);
for (int i = 1; i < fields.length; i++) {
    // variable referenced in lambda must be final or effectively final
    final Field f = fields[i];
    
    comparator = comparator.thenComparing((Person person) -> {
        try {
            return (Comparable) f.get(person);
        } catch(IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    });
}

This will cause warnings about an unchecked method invocation and an unchecked conversion, which can be suppressed with a @SuppressWarnings("unchecked") annotation on the method. The warning is real: if Person has a field whose type is not Comparable then this will fail at runtime. You may wish to check whether f's type implements Comparable first, and only update the comparator if it does; I have not done this for you.

Note also that the code which loops starting from index 1 is liable to break if id ever stops being the first field in the array; I suggest simply looping over the whole array, which will superfluously include the id field a second time but this does no harm except for a small performance cost.

Related