Comparator
В прошлой теме Collections.sort расставлял строки по кодам символов, а объекты вашего класса — так, как решил их compareTo. Это естественный порядок, и у класса он один. А если тот же список слов нужно отсортировать по длине? Переписать compareTo у String нельзя: класс не ваш.
Вернёмся к конвейеру. На ленте стоит станция сортировки: работник берёт две коробки и решает, какая встанет раньше. Порядок, напечатанный на самой коробке, — это Comparable из прошлой темы. А карточку с правилом сравнения, например «короче раньше», дают отдельно, и станции можно вручить любую. В Java такая карточка называется Comparator:
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]
}
}Давайте разберём программу по строкам:
Comparator<String> byLength— карточка «как сравнить две строки». У интерфейсаComparatorодин абстрактный методcompare(a, b), поэтому его записывают лямбдой с двумя параметрами.Integer.compare(a.length(), b.length())сравнивает длины двух слов.words.sort(byLength)сортирует сам список по этой карточке. Метод ничего не возвращает и меняетwordsна месте.- Печать даёт
[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() возвращает новый компаратор с обратным порядком: от длинного слова к короткому. Тот компаратор, у которого его вызвали, не меняется.
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:
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]Разберём:
- Первое правило — длина:
Boкороче всех,Annaдлиннее всех. - У
ZoeиMaxпо три буквы, и первое правило их не различает. Comparator.naturalOrder()— естественный порядок строк, тот самыйcompareTo, который сравнивает коды символов. ДляMaxиZoeэто обычный алфавит, иMaxвстаёт раньше.
На собеседовании часто спрашивают, чем Comparable отличается от Comparator. Ответ: Comparable даёт классу один естественный порядок изнутри, через compareTo. Comparator — отдельный объект, и порядков для одного класса можно завести сколько угодно, не трогая сам класс.
Здесь картинка со станцией неточна. Другие станции работают с каждой коробкой, пока та проезжает мимо, а сортировке нужны все коробки сразу. Поэтому sort переставляет весь список, а компаратор лишь отвечает, какая из двух коробок раньше.
Частая ошибка
В comparing передали лямбду и сразу развернули порядок:
words.sort(Comparator.comparing(w -> w.length()).reversed()); // ✗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)сортирует сам список и ничего не возвращает.