Объясните, почему ConcurrentHashMap запрещает null-ключи и null-значения, хотя HashMap их допускает.
ConcurrentHashMap запрещает null, чтобы результат get однозначно означал одно из двух: найдено ненулевое значение или отображение отсутствует. В конкурентной среде нельзя надёжно трактовать null как допустимое значение и одновременно как признак отсутствия ключа: отдельная проверка через containsKey уже может устареть.
Коллекции вроде HashMap рассчитаны прежде всего на обычный однопоточный доступ и допускают null как специальное значение. Для многопоточных сценариев в Java появились специализированные структуры, включая ConcurrentHashMap, чтобы обеспечить безопасный конкурентный доступ без внешней блокировки всей коллекции.
Такой дизайн должен был поддерживать простые атомарные операции и предсказуемую семантику чтения. Запрет null устраняет неоднозначность, которая особенно опасна при изменениях коллекции из нескольких потоков.
Если get может вернуть null и для реально сохранённого значения, и для отсутствующего ключа, вызывающему коду приходится дополнительно проверять наличие ключа. Между этими двумя операциями другой поток может удалить или добавить отображение, поэтому результат проверки уже не обязательно описывает состояние, к которому относился предыдущий get.
Неверное решение приводит к ошибочной логике кэша, повторным вычислениям, преждевременному созданию объекта или неправильному выводу об отсутствии данных. Проблема не в том, что null невозможно физически хранить, а в том, что его присутствие разрушает однозначный контракт конкурентного чтения.
В ConcurrentHashMap возвращаемый null однозначно означает отсутствие отображения. Поэтому методы чтения и атомарные составные операции могут использовать ненулевое значение как признак успешного наличия записи, не вводя дополнительное состояние для различения двух смыслов null.
Кроме того, методы вроде put, putIfAbsent и вычисляющие операции требуют ненулевых ключей и значений. Функция, переданная в computeIfAbsent, не должна возвращать null: такой результат означает, что отображение добавлять не нужно.
Если бизнес-модель действительно допускает значение «известно, что результата нет», его нужно представить явно. Например, можно использовать Optional как значение, специальный объект-сентинел или отдельную модель состояния.
Здесь null от get означает отсутствие ключа, а Optional.empty() — явно сохранённое состояние «значение отсутствует». Компромисс состоит в дополнительной оболочке и необходимости дисциплинированно обрабатывать её во всех местах доступа.
Сервис кэширует результаты дорогостоящего поиска. Пустой результат также нужно кэшировать, иначе каждый запрос к отсутствующему объекту снова запускает дорогое вычисление.
Первый вариант — хранить результат в ConcurrentHashMap напрямую. Он не подходит, потому что null нельзя поместить в карту, а попытка различать отсутствие записи и пустой результат через отдельный флаг создаёт гонку между операциями.
Второй вариант — использовать специальный объект-сентинел. Он эффективен по памяти и не требует Optional, но усложняет типизацию и повышает риск случайно вернуть внутренний маркер наружу.
Выбран вариант с Optional в качестве значения. Он явно разделяет состояния «ключ отсутствует», «результат есть» и «результат вычислен, но пуст», а атомарное заполнение можно выполнять через computeIfAbsent. Цена решения — дополнительный объект или упаковка значения, что обычно оправдано для корректности кэша.
null последовательностью containsKey и get?Нет, такая последовательность не становится атомарной только потому, что обе операции выполняются над ConcurrentHashMap. Между ними другой поток может изменить отображение, поэтому полученная пара результатов может относиться к разным состояниям карты. Если решение зависит от единой проверки и изменения, нужно использовать подходящую атомарную операцию, например putIfAbsent или computeIfAbsent.
get со значением null, что ключа точно нет во всех конкурентных коллекциях?Нет, это утверждение специфично для коллекций с соответствующим контрактом. У ConcurrentHashMap null действительно означает отсутствие отображения, потому что null запрещён. Нельзя механически переносить эту семантику на произвольную реализацию Map, допускающую null.
Optional.empty() и проверять только его?Потому что Optional.empty() и null обозначают разные состояния. Optional.empty() означает, что ключ присутствует и результат уже определён как пустой, тогда как null от get означает отсутствие ключа. Если код проверяет только isEmpty() без предварительной проверки самого объекта, он может получить NullPointerException при обращении к отсутствующему ключу.