Почему Comparator.comparing с getter'ом иногда даёт NPE там, где прямой вызов поля не давал
Вопрос звучит безобидно: отсортируй список сущностей по дате создания. Кандидат пишет list.sort(Comparator.comparing(Entity::getCreatedAt)) и получает NPE на проде, хотя в тестах всё работало.
Разница в данных: в тестах createdAt всегда проставлен, в проде часть записей — из старой миграции без значения в этом поле. comparing() вызывает compareTo прямо на результате getter'а, без всякой защиты от null. Прямое сравнение через if в ручном Comparator хотя бы заставляло задуматься о null-проверке — ленивый comparing() эту проверку прячет.
Правильный ответ — не просто обернуть в try-catch, а решить, что делать с null семантически: нужны они в начале сортировки или в конце.
list.sort(Comparator.comparing( Entity::getCreatedAt, Comparator.nullsLast(Comparator.naturalOrder()) ));
Следующий вопрос обычно про thenComparing: что будет, если два объекта равны по первому полю. Ответ — comparator идёт по цепочке только если предыдущий compare вернул 0, и если thenComparing не указан, порядок равных элементов undefined для несортировки или сохраняется при stable sort — путают именно это.
nullsLast не чинит данные, он просто честно объявляет, куда деть null — в начало или конец.
senior·base — что спрашивают на самом деле
В этом посте были ссылки, но мы их удалили по правилам Сетки