Почему 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 — что спрашивают на самом деле
В этом посте были ссылки, но мы их удалили по правилам Сетки