Допустим, функция вычисления значения для отсутствующего ключа возвращает null. Как изменится состояние Map после computeIfAbsent?
Если функция, переданная в computeIfAbsent, возвращает null, отображение для ключа не добавляется и существующая запись с null не создаётся. Сам вызов возвращает null. Если для ключа уже было ненулевое значение, функция вообще не вызывается, а это значение сохраняется.
Метод computeIfAbsent появился в Java 8 вместе с набором методов для условного вычисления и обновления значений в Map. Он решает распространённую задачу ленивого создания значения без отдельной ручной проверки наличия ключа.
До этого разработчику приходилось самостоятельно разделять проверку карты и вычисление значения. Такой код был многословнее, а в многопоточном сценарии мог содержать гонку между проверкой и добавлением.
Возврат null часто означает, что значение вычислить не удалось или его не следует кэшировать. Если ошибочно ожидать, что computeIfAbsent сохранит null, последующие вызовы снова будут считать ключ отсутствующим и повторно запускать функцию вычисления.
Особенно заметно это в кэшах: отсутствие записи и наличие записи со значением null становятся неразличимыми через обычную семантику computeIfAbsent. Неверное понимание контракта может привести к повторным обращениям к базе данных, внешнему сервису или дорогим вычислениям.
Для ключа, связанного с ненулевым значением, computeIfAbsent возвращает это значение без вызова функции. Для отсутствующего ключа или ключа, отображённого в null, функция вычисляется.
Если функция возвращает ненулевой результат, он записывается в карту и возвращается вызывающему коду. Если результат равен null, запись не создаётся или не изменяется, а метод возвращает null.
В примере ключ остаётся отсутствующим. Поэтому повторный вызов снова может выполнить функцию вычисления.
У обычного HashMap этот метод не делает составную операцию автоматически потокобезопасной. В ConcurrentHashMap вычисление для одного ключа имеет более строгую атомарную семантику, но функция не должна долго выполняться, рекурсивно изменять ту же карту или зависеть от побочных эффектов без необходимости.
Если бизнес-логике нужно кэшировать факт отсутствия результата, null использовать как такое значение нельзя. Обычно применяют специальный объект-заменитель, например отдельный объект-сентинел, либо хранят результат в оболочке, способной явно представлять состояние отсутствия.
Сервис кэширует результат поиска пользователя. Для несуществующего пользователя функция поиска возвращает null. При использовании computeIfAbsent отрицательный результат не сохраняется, поэтому каждый повторный запрос снова обращается к базе данных.
Рассматривались следующие варианты:
containsKey и последующее добавление — позволяет выразить нужную семантику, но создаёт больше кода и требует отдельной синхронизации в многопоточном случае;putIfAbsent — не вычисляет значение лениво и не решает задачу самостоятельно;null, но требует корректной обработки сентинела при чтении;Выбран объект-сентинел с ограниченным временем жизни отрицательной записи. Это предотвращает повторные запросы к базе, а время жизни ограничивает риск длительно кэшировать временную ошибку или недавно созданного пользователя.
Вызывается ли функция, если ключ уже связан со значением null?
Да. Для computeIfAbsent отображение в null рассматривается так же, как отсутствие ненулевого значения: функция будет вызвана. Если она вернёт ненулевой результат, он заменит null в карте.
Гарантирует ли обычный HashMap однократное вычисление значения при параллельных вызовах?
Нет. Контракт Map не делает computeIfAbsent потокобезопасным. Несколько потоков могут одновременно увидеть отсутствие значения и выполнить функцию; для безопасной конкурентной семантики нужна подходящая concurrent-реализация, например ConcurrentHashMap, с учётом её ограничений.
Можно ли использовать computeIfAbsent для кэширования результата, представленного null?
Нельзя напрямую: null означает, что отображение не должно быть установлено. Для кэширования такого результата нужен ненулевой маркер — например, объект-обёртка с состоянием отсутствия или специальный неизменяемый сентинел. При чтении этот маркер необходимо преобразовать обратно в бизнес-смысл null или отсутствия результата.