Почему Collections.max на кастомном компараторе иногда падает с IllegalArgumentException

Вопрос звучит просто: дан список объектов, нужен максимум по нескольким полям — сначала по дате, потом по сумме. Кандидат пишет компаратор через вычитание long-полей и получает рабочий код. До первого большого значения.

Comparator byAmount = (a, b) -> (int) (a.getAmount()

  • b.getAmount());

Amount — long, разница может выйти за границы int и переполниться. Компаратор перестаёт быть транзитивным: a больше b, b больше c, а c больше a. TimSort, на котором работает Collections.sort и Collections.max с обходом всей коллекции, в какой-то момент детектирует нарушение инварианта и бросает IllegalArgumentException: Comparison method violates its general contract.

Проблема не в том, что сравнение через вычитание «неправильное» само по себе — для int-полей в разумных пределах оно работает годами и в тестах, и в проде. Ловится только на реальных данных, где разница выходит за диапазон int.

Дальше спросят: как написать безопасный компаратор для составного ключа. Правильный ответ — Long.compare(a, b) для каждого поля или Comparator.comparingLong(...).thenComparing(...). Ещё спросят, почему TimSort вообще способен заметить нарушение транзитивности — там есть проверка инварианта merge на этапе слияния, а не отдельный анализ компаратора.

Компаратор через вычитание — тот код, который проходит ревью, потому что выглядит компактно и работает на тестовых данных с маленькими числами.

senior·base — что спрашивают на самом деле


В этом посте были ссылки, но мы их удалили по правилам Сетки