Статья

Сортировка коллекции по нескольким полям в Java

При программировании нам часто приходится сортировать коллекции объектов. Логика сортировки иногда может оказаться трудной для реализации, если мы хотим отсортировать объекты по нескольким полям. В этой статье мы обсудим несколько различных подходов к решению проблемы, а также их плюсы и минусы.

Давайте определим класс Student с двумя полями: name и age. В наших примерах мы будем сравнивать объекты Student сначала по имени, а затем по возрасту:
public class Student {
    @Nonnull private String name;
    private int age;

    // конструктор

    // геттеры и сеттеры
}
Здесь мы добавили аннотацию @Nonnull, чтобы упростить примеры. Но в продакшн коде нам может потребоваться сравнивать поля, допускающие null.
Java предоставляет интерфейс Comparator для сравнения двух объектов одного и того же типа. Мы можем реализовать его метод compare(T o1, T o2) с настраиваемой логикой для нужного сравнения.

Проверка разных полей по порядку

Давайте сравним поля одно за другим:
public class CheckFieldsOneByOne implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        int nameCompare = o1.getName().compareTo(o2.getName());
        if(nameCompare != 0) {
            return nameCompare;
        }
        return Integer.compare(o1.getAge(), o2.getAge());
    }
}
Здесь мы используем метод compareTo() класса String и метод compare() класса Integer для сравнения полей name и age одно за другим.
Требуется много кода, а иногда и обработка множества особых случаев. Поэтому код трудно поддерживать и масштабировать, когда есть много полей для сравнения. Как правило, не рекомендуется использовать этот метод в продакшн коде.

Использование ComparisonChain из библиотеки Guava

Давайте добавим зависимость от библиотеки Google Guava в наш pom.xml:
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.1-jre</version>
</dependency>
Мы можем упростить логику, используя класс ComparisonChain из этой библиотеки:
public class ComparisonChainExample implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        return ComparisonChain.start()
          .compare(o1.getName(), o2.getName())
          .compare(o1.getAge(), o2.getAge())
          .result();
    }
}
Здесь мы используем методы compare(int left, int right) и compare(Comparable<?> left, Comparable<?> right) в ComparisonChain для сравнения имени и возраста соответственно.
Такой подход скрывает детали сравнения и раскрывает только то, что нас волнует — поля, которые мы хотели бы сравнить, и порядок, в котором они должны сравниваться. Также мы должны отметить, что нам не нужна никакая дополнительная логика для обработки null, поскольку об этом заботятся библиотечные методы. Таким образом, код становится легче обслуживать и масштабировать.

Сортировка с помощью Apache Commons CompareToBuilder

Для начала добавим зависимость от библиотеки Apache Commons в наш pom.xml:
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.12.0</version>
</dependency>
Как и в предыдущем примере, мы можем использовать CompareToBuilder из Apache Commons, чтобы сократить шаблонный код:
public class CompareToBuilderExample implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        return new CompareToBuilder()
          .append(o1.getName(), o2.getName())
          .append(o1.getAge(), o2.getAge())
          .build();
    }
}
Этот подход очень похож на ComparisonChain от Guava — он также скрывает детали сравнения и легко поддерживается и масштабируется.

Использование Comparator.comparing() и Lambda выражений

Начиная с Java 8, в интерфейс Comparator добавлено несколько статических методов, которые могут использовать лямбда-выражения для создания объекта Comparator. Мы можем использовать метод comparing() для построения нужного нам компаратора:
public static Comparator<Student> createPersonLambdaComparator() {
    return Comparator.comparing(Student::getName)
      .thenComparing(Student::getAge);
}
Этот подход гораздо более лаконичен и удобочитаем, поскольку он напрямую использует геттеры класса Student.
Этот код легко поддерживать и масштабировать. Кроме того, геттеры здесь вызываются лениво по сравнению с предыдущими подходами. В результате его производительность выше и больше подходит для систем, чувствительных к задержке, которые требуют большого количества сравнений больших объемов данных.
Более того, этот подход использует только основные классы Java и не требует каких-либо сторонних библиотек в качестве зависимостей. В целом, это наиболее рекомендуемый подход.

Заключение

В этой статье мы изучили основные подходы к сравнению по нескольким полям в сортировке коллекций объектов.
java