iamjoshlee
iamjoshlee

Reputation: 33

Dynamically chain comparators in Java

I would like to be able to sort a list dynamically by the various, selectable properties of an element that may be passed into a method.

For example, lets say I have a Person class:

interface Person {
    String getName();
    int getAge();
    LocalDate getBirthDate();
}

If I want to be able to sort by name first, then by age second, I would normally do this:

Collections.sort(unsortedPersons, Comparator.comparing(Person::getFirstName).thenComparing(Person::getAge))

But what if I wanted to select those properties dynamically? How could this properly be achieved? I want to be able to have a contract similar to:

List<Person> sortedPersons = sortPersons(
        List.of(person1, person2, person3), 
        List.of(Person::getName, Person::getAge)
);

where the first parameter is the unsorted list of persons, and the second parameter is a list of functions that will create the above comparator chain dynamically.

The problem I have is that the method references below can't resolve, likely due to the fact that the list of functions can return different types.

public static List<Person> sortPersons(List<Person> unsortedPersons, List<Function<Person, ?>> sortableproperties) {
    var comparator = sortableproperties.stream()
            .map(Comparator::comparing)
            .reduce(Comparator::thenComparing)
            .orElse(Comparator.naturalOrder());
   return unsortedPersons.stream()
           .sorted(comparator)
           .collect(Collectors.toList());

}

Is there a more elegant way to achieve this?

Upvotes: 3

Views: 346

Answers (1)

samabcde
samabcde

Reputation: 8114

The main problem is the method sortPersons parameter List<Function<Person, ?>> which compiler can't recognise when call Comparator.comparing. Unfortunately, this problem can't be solved by replacing ? to Comparable<?>. So the best we can do is to convert the Function to Comparator<Person> first.

It may sounds a bit boring if we only have something like

private static <T extends Comparable<T>> Comparator<Person> convertToComparator(Function<Person, T> properties) {
    return Comparator.comparing(properties);
}

But it is quite common we need to switch the sort to ascending/descending, so we can write below.

private static <T extends Comparable<T>> Comparator<Person> asc(Function<Person, T> properties) {
    return Comparator.comparing(properties);
}

private static <T extends Comparable<T>> Comparator<Person> desc(Function<Person, T> properties) {
    return Comparator.comparing(properties).reversed();
}

Then we can do like this.

List<Person> sortedPersons = sortPersons(
        List.of(person1, person2, person3),
        List.of(asc(Person::getName), desc(Person::getAge))
);

Upvotes: 1

Related Questions