Comparator в Java: сортировка по нескольким полям и обработка null

Comparator в Java: сортировка по нескольким полям и обработка null
Содержание статьи

Comparator описывает внешний порядок сравнения объектов. Через comparing выбирают основной ключ, через thenComparing добавляют следующий, а через nullsLast задают место отсутствующих значений. Следующий ключ используется только при равенстве предыдущего.

В учебном примере Mentoring.digital задачи нужно расположить сначала по возрастанию приоритета, затем по названию. Чем меньше число приоритета, тем выше задача в списке. Названия сравниваем обычным порядком Java-строк, без правил русского словаря.

Сортировка по двум полям

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class SortDemo {
    record Task(int priority, String name) {}

    public static void main(String[] args) {
        var tasks = new ArrayList<>(List.of(
            new Task(2, "Docs"),
            new Task(1, "Tests"),
            new Task(1, "Build")
        ));
        Comparator<Task> order = Comparator
            .comparingInt(Task::priority)
            .thenComparing(Task::name);
        tasks.sort(order);
        tasks.forEach(System.out::println);
    }
}

Пример с record рассчитан на Java 16 и новее. Получится порядок Build, Tests, Docs. Задача Docs остаётся последней, хотя её название идёт раньше Tests: основной ключ уже определил порядок.

tasks.sort изменяет сам список. Если исходный порядок нужен для другой операции, создайте new ArrayList<>(tasks) и сортируйте копию. Копирование списка не создаёт глубокие копии его элементов.

Null в поле и null вместо объекта

Comparator<Task> byFields = Comparator
    .comparingInt(Task::priority)
    .thenComparing(Task::name,
        Comparator.nullsLast(Comparator.naturalOrder()));

Comparator<Task> withNullTasks = Comparator.nullsLast(byFields);

Первый nullsLast относится к названию. Задачи с отсутствующим названием попадут в конец своей группы приоритета. Внешний nullsLast обрабатывает ситуацию, когда элемент списка целиком равен null. Это две разные проверки. Если nullable только поле, внешняя обёртка не нужна.

Как правильно развернуть порядок

Comparator<Task> priorityDescending = Comparator
    .comparingInt(Task::priority)
    .reversed()
    .thenComparing(Task::name,
        Comparator.nullsLast(Comparator.naturalOrder()));

Здесь развёрнут только приоритет, а названия остаются по возрастанию. Если вызвать reversed() в конце всей цепочки, развернутся все её критерии. Разворот компаратора с nullsLast также поменяет место null. Чтобы null-объекты всегда оставались последними, внешнюю обёртку применяйте уже к нужному окончательному порядку.

Ошибки и проверка результата

Не пишите сравнение целых чисел как a.priority() - b.priority(): при больших значениях возможно переполнение. Используйте comparingInt или Integer.compare. Компаратор должен давать согласованный порядок, включая равные значения и повторные сравнения.

Проверьте разные приоритеты, одинаковый приоритет с разными именами, одинаковые ключи и допустимые null. Если тот же компаратор используется в TreeSet, помните: результат сравнения ноль определяет эквивалентность элементов для множества. Два разных объекта с одинаковыми ключами могут не сохраниться как две записи.

Для системной практики с коллекциями есть курсы Java с наставником. Правила сортировки реальных объектов можно обсудить с менторами по Java.

Похожие статьи по данной теме