Перейти к содержимому
Теория · Лямбды и Stream API · урок 5 из 12

Comparator

В прошлой теме Collections.sort расставлял строки по кодам символов, а объекты вашего класса — так, как решил их compareTo. Это естественный порядок, и у класса он один. А если тот же список слов нужно отсортировать по длине? Переписать compareTo у String нельзя: класс не ваш.

Вернёмся к конвейеру. На ленте стоит станция сортировки: работник берёт две коробки и решает, какая встанет раньше. Порядок, напечатанный на самой коробке, — это Comparable из прошлой темы. А карточку с правилом сравнения, например «короче раньше», дают отдельно, и станции можно вручить любую. В Java такая карточка называется Comparator:

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

public class Main {
    public static void main(String[] args) {
        List<String> words =
            new ArrayList<>(List.of("kiwi", "fig", "banana"));
        Comparator<String> byLength =
            (a, b) -> Integer.compare(a.length(), b.length());
        words.sort(byLength);
        System.out.println(words); // [fig, kiwi, banana]
    }
}

Давайте разберём программу по строкам:

  1. Comparator<String> byLength — карточка «как сравнить две строки». У интерфейса Comparator один абстрактный метод compare(a, b), поэтому его записывают лямбдой с двумя параметрами.
  2. Integer.compare(a.length(), b.length()) сравнивает длины двух слов.
  3. words.sort(byLength) сортирует сам список по этой карточке. Метод ничего не возвращает и меняет words на месте.
  4. Печать даёт [fig, kiwi, banana]: от короткого слова к длинному.

Метод compare

Компаратор — объект типа Comparator, который сравнивает два значения. Его метод compare(a, b) отвечает числом, как compareTo из прошлой темы:

  • отрицательное число — a встаёт раньше b;
  • ноль — для этого порядка a и b равны;
  • положительное число — a встаёт позже b.

Ответ Integer.compare устроен именно так. Вычитание a - b на больших числах переполняется, как счётчик пробега из темы про операторы, поэтому числа сравнивают через Integer.compare.

Сортировать по компаратору умеют и list.sort(c), и Collections.sort(list, c). Оба меняют сам список и ничего не возвращают.

Comparator.comparing

Чаще карточку не пишут вручную, а строят по ключу — тому, по чему сравнивают. Comparator.comparing(String::length) достаёт из каждой строки длину и сравнивает длины, то есть делает то же, что byLength. Готовый компаратор можно развернуть или дополнить вторым правилом.

reversed

reversed() возвращает новый компаратор с обратным порядком: от длинного слова к короткому. Тот компаратор, у которого его вызвали, не меняется.

JAVA
words.sort(Comparator.comparing(String::length));
System.out.println(words); // [fig, kiwi, banana]
words.sort(Comparator.comparing(String::length).reversed());
System.out.println(words); // [banana, kiwi, fig]

thenComparing

Если два значения по первому правилу равны, их порядок решает второе — thenComparing:

JAVA
List<String> names = new ArrayList<>(List.of("Zoe", "Anna", "Max", "Bo"));
names.sort(Comparator.comparing(String::length)
    .thenComparing(Comparator.naturalOrder()));
System.out.println(names); // [Bo, Max, Zoe, Anna]

Разберём:

  1. Первое правило — длина: Bo короче всех, Anna длиннее всех.
  2. У Zoe и Max по три буквы, и первое правило их не различает.
  3. Comparator.naturalOrder() — естественный порядок строк, тот самый compareTo, который сравнивает коды символов. Для Max и Zoe это обычный алфавит, и Max встаёт раньше.

На собеседовании часто спрашивают, чем Comparable отличается от Comparator. Ответ: Comparable даёт классу один естественный порядок изнутри, через compareTo. Comparator — отдельный объект, и порядков для одного класса можно завести сколько угодно, не трогая сам класс.

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

Частая ошибка

В comparing передали лямбду и сразу развернули порядок:

JAVA
words.sort(Comparator.comparing(w -> w.length()).reversed()); // ✗
TEXT
error: cannot find symbol

Программа не компилируется. Тип параметра w компилятор выводит по тому, куда отдают лямбду.

Без .reversed() компаратор уходил бы прямо в words.sort, и w был бы строкой. Здесь же результат comparing сначала получает reversed(), и о списке строк comparing ничего не знает. Поэтому w получает тип Object, а у Object нет метода length() — следующая строка сообщения так и говорит: location: variable w of type Object. Исправление — ссылка на метод String::length или тип в лямбде: (String w) -> w.length().

Главное

  • Comparator — отдельный объект-сравнение: compare(a, b) возвращает отрицательное число, ноль или положительное.
  • Comparable даёт классу один естественный порядок, а компараторов для того же класса можно написать сколько угодно.
  • Comparator.comparing(ключ) строит компаратор по ключу, reversed() разворачивает порядок, thenComparing решает ничьи.
  • list.sort(c) сортирует сам список и ничего не возвращает.