Интерфейсы Comparable и Comparator в Java - Вопросы
Всего: 6 вопросов
1. Чем отличаются Comparable и Comparator в Java и когда что использовать?
Чем отличаются Comparable и Comparator в Java и когда что использовать?
Comparable<T> (пакет java.lang) задаёт естественный порядок класса: класс сам реализует int compareTo(T o) и сравнивает this с аргументом. Такой порядок у класса один. Его уже реализуют String, Integer, LocalDate, BigDecimal и все enum.
Comparator<T> (пакет java.util) — внешнее правило сравнения: int compare(T o1, T o2) реализуют в отдельном классе, анонимном классе или лямбдой, и компараторов для одного класса может быть сколько угодно. Менять сам класс не нужно, поэтому компаратор подходит для String и чужих классов. Comparator — функциональный интерфейс с готовыми методами Java 8: comparing(), thenComparing(), reversed().
Если порядок очевиден и единственный (числа, даты, коды), реализуют Comparable; для остальных случаев пишут компаратор. Подходы совмещаются: переданный компаратор (list.sort(cmp), new TreeSet<>(cmp)) всегда имеет приоритет над естественным порядком, а без него (или с null) используется compareTo().
2. Что возвращают compareTo() и compare() и какой контракт они должны соблюдать?
Что возвращают compareTo() и compare() и какой контракт они должны соблюдать?
Оба метода возвращают int: отрицательное число — первый объект (this или o1) стоит перед вторым, положительное — после, ноль — объекты равны для этого порядка (что не обязательно означает equals() == true).
Важен только знак, а не значение: String.compareTo() возвращает разность кодов символов, поэтому проверка a.compareTo(b) == 1 — ошибка, правильно a.compareTo(b) > 0.
Контракт:
- знак
a.compareTo(b)противоположен знакуb.compareTo(a); - порядок транзитивен: если
a > bиb > c, тоa > c; - элементы, равные при сравнении, одинаково сравниваются с любым третьим.
При нарушении контракта сортировка даёт неверный результат или падает с IllegalArgumentException: Comparison method violates its general contract!.
3. Почему в compareTo() и compare() нельзя писать return a - b и чем это заменить?
Почему в compareTo() и compare() нельзя писать return a - b и чем это заменить?
Вычитание даёт правильный знак только пока разность помещается в int. При переполнении знак меняется: Integer.MIN_VALUE - 1 превращается в Integer.MAX_VALUE (2147483647), и компаратор (a, b) -> a - b решает, что минус два миллиарда больше единицы. Список [1, -2147483648] после такой сортировки остаётся неотсортированным, а нарушенный контракт может привести к IllegalArgumentException.
Вместо вычитания используют:
Integer.compare(this.age, other.age),Long.compare(),Double.compare();- ссылку на метод
Integer::compareкак компаратор; - в цепочках —
Comparator.comparingInt(),comparingLong(),comparingDouble(): внутри они вызываютcompare()и не делают автоупаковку.
Тесты на маленьких числах такую ошибку не ловят: она проявляется на идентификаторах, суммах в копейках или отрицательных значениях.
4. Что значит «compareTo согласован с equals» и как это влияет на TreeSet и TreeMap?
Что значит «compareTo согласован с equals» и как это влияет на TreeSet и TreeMap?
Порядок согласован с equals(), если a.compareTo(b) == 0 тогда и только тогда, когда a.equals(b). Документация Java настоятельно рекомендует это, но не требует.
На сортировку списка нарушение не влияет, но TreeSet и ключи TreeMap ищут и отбрасывают дубликаты по compareTo()/compare(), а не по equals(): если сравнение вернуло 0, новый элемент считается уже существующим, и add() возвращает false без исключения. HashSet при этом опирается на equals() и hashCode().
Пример из стандартной библиотеки — BigDecimal: new BigDecimal("1.0") и new BigDecimal("1.00") не равны по equals() (учитывается масштаб), но compareTo() возвращает 0. В HashSet будет два элемента, в TreeSet — один. Так же компаратор только по фамилии «теряет» второго Иванова. Если объекты хранятся в TreeSet или служат ключами TreeMap, в сравнение включают все поля, которые участвуют в equals(), например через thenComparing().
5. Как собрать компаратор методами Comparator.comparing, thenComparing и reversed и как отсортировать по убыванию?
Как собрать компаратор методами Comparator.comparing, thenComparing и reversed и как отсортировать по убыванию?
С Java 8 компаратор собирают из статических и default-методов интерфейса Comparator:
comparing(Person::getLastName)— сравнение по ключу (ключ должен бытьComparable); вторым аргументом можно передать компаратор для ключа;comparingInt()/comparingLong()/comparingDouble()— по примитивному ключу, без автоупаковки и переполнения;thenComparing(),thenComparingInt()— следующий критерий, если предыдущий вернул 0;reversed()— обратный порядок всей цепочки, к которой применён;naturalOrder()/reverseOrder()— естественный порядок и обратный ему;nullsFirst(cmp)/nullsLast(cmp)—nullв начало или в конец, без нихnullв списке вызываетNullPointerException.
people.sort(Comparator.comparing(Person::getLastName)
.thenComparingInt(Person::getAge)); // фамилия, затем возраст
people.sort(Comparator.comparingInt(Person::getAge).reversed()); // по убыванию возраста
names.sort(Comparator.reverseOrder()); // строки по убыванию
Чтобы перевернуть только один критерий цепочки, его передают вторым аргументом: thenComparing(Person::getAge, Comparator.reverseOrder()).
6. Как String.compareTo() сравнивает строки и как отсортировать русские слова по алфавиту?
Как String.compareTo() сравнивает строки и как отсортировать русские слова по алфавиту?
String.compareTo() сравнивает строки посимвольно по кодам UTF-16 и возвращает разность первых несовпавших символов. Поэтому заглавные буквы идут раньше строчных, а «Ё» (U+0401) — даже раньше «А» (U+0410): естественный порядок списка яблоко, Ёлка, арбуз, Банан — [Ёлка, Банан, арбуз, яблоко].
String.CASE_INSENSITIVE_ORDER убирает разницу регистров, но «Ёлка» оказывается в конце: строчная «ё» (U+0451) стоит в таблице после «я» (U+044F). Результат — [арбуз, Банан, яблоко, Ёлка].
Словарный порядок даёт Collator: он реализует Comparator и учитывает правила языка.
words.sort(Collator.getInstance(Locale.forLanguageTag("ru")));
// [арбуз, Банан, Ёлка, яблоко]