В какой момент одного результата Map.get недостаточно, чтобы понять, существует ли отображение ключа?
Одного результата Map.get недостаточно, если карта допускает null-значения: null означает либо отсутствие отображения, либо наличие ключа со значением null. Для различения этих случаев нужно дополнительно вызвать containsKey.
Если карта запрещает null-значения по своему контракту, неоднозначность устраняется ограничениями конкретной реализации, но полагаться на это можно только после проверки её документации.
Интерфейс Map описывает отображение ключей в значения и допускает реализации с разными ограничениями на null. Такой контракт позволяет одной реализации, например HashMap, хранить null-ключи и null-значения, а другой, например ConcurrentHashMap, запрещать их.
Из-за совместимости общего API метод get возвращает значение либо null, если отображение не найдено. Поэтому сам результат не кодирует отдельно состояния «ключ отсутствует» и «ключ присутствует, но значение равно null».
Предположим, бизнес-логика использует null как специальное значение: например, ключ присутствует в кэше, но результат вычисления пока неизвестен. Проверка только результата get ошибочно примет такой ключ за отсутствующий.
Последствия зависят от сценария: можно повторно выполнить дорогостоящий расчёт, заменить существующее состояние значением по умолчанию или неверно сообщить об отсутствии данных. Ошибка особенно незаметна, потому что формально вызов get завершается корректно.
Проверка должна разделять два состояния:
containsKey(key) возвращает false — отображения нет.containsKey(key) возвращает true, а get(key) возвращает null — отображение есть, его значение равно null.Минимальный пример:
Вызов get и последующая проверка containsKey обычно выполняются за ожидаемое O(1) в HashMap, но это два отдельных поиска. Для конкурентной карты важно учитывать атомарность: между двумя вызовами другой поток может изменить отображение. Если проверка и действие должны быть единым неделимым этапом, следует использовать подходящий составной метод или внешнюю синхронизацию согласно контракту конкретной карты.
Метод getOrDefault не устраняет проблему полностью: он возвращает значение по умолчанию при отсутствии ключа, но если ключ есть и его значение равно null, результатом всё равно будет null. При необходимости различать состояния нужно проверять наличие ключа отдельно либо хранить специальный объект-обёртку, кодирующий состояние явно.
В кэше хранятся результаты проверки конфигурации. null означает «проверка выполнена, но значение отсутствует», а отсутствие ключа означает «проверка ещё не запускалась».
Вариант с одной проверкой get прост и быстр, но теряет различие состояний. Вариант с getOrDefault удобен для обычных значений, однако не решает задачу при допустимом null. Обёртка вроде Optional или отдельный объект состояния делает модель явнее, но увеличивает количество объектов и усложняет API.
Для обычной однопоточной HashMap выбранное решение — проверять containsKey, когда бизнес-логике действительно важно различать состояния. В многопоточном кэше проверку и последующее изменение проектируют как атомарную операцию средствами выбранной concurrent-реализации, а не как независимую пару вызовов. Это предотвращает ошибочные решения на устаревшем состоянии.
Дополнительный вопрос: меняет ли getOrDefault поведение при наличии ключа со значением null?
Нет. Если ключ присутствует и его значение равно null, результат getOrDefault также может быть null; значение по умолчанию не используется как средство различения всех состояний. Поэтому для карты, допускающей null, наличие ключа по-прежнему проверяют через containsKey.
Дополнительный вопрос: можно ли считать get безопасной проверкой отсутствия в ConcurrentHashMap?
Только если контракт карты запрещает null-значения и это ограничение гарантировано выбранной реализацией. Для ConcurrentHashMap null-ключи и null-значения запрещены, поэтому get может использоваться для различения присутствия и отсутствия значения, но результат всё равно может устареть сразу после чтения из-за конкурентных изменений.
Дополнительный вопрос: почему проверка containsKey, а затем get не всегда корректна в многопоточном коде?
Между двумя вызовами другой поток может удалить ключ, заменить значение или вставить новое отображение. В результате второй вызов уже не обязательно относится к состоянию, подтверждённому первым. Если требуется атомарное «проверить и выполнить действие», нужно применять специализированный атомарный метод карты, синхронизацию или иной механизм координации, соответствующий задаче.