Почему 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 — что спрашивают на самом деле


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