Map.get возвращает null: почему падает addAll и как исправить контракт
Автор: Казачкин Даниил Михайлович · Обновлено
Сообщение Cannot invoke "java.util.List.addAll(java.util.Collection)" because the return value of "java.util.Map.get(Object)" is null указывает на конкретное звено выражения: Map…
Сообщение Cannot invoke "java.util.List.addAll(java.util.Collection)" because the return value of "java.util.Map.get(Object)" is null указывает на конкретное звено выражения: Map вернула null, и программа попыталась вызвать на нём addAll. Исправлять нужно правило создания или проверки списка по ключу, а не сам метод добавления.
Разложите цепочку на наблюдаемые значения
В выражении groups.get("java").addAll(incoming) сначала вычисляется get, затем его результат используется как получатель вызова. Если groups сама равна null, причина другая; если получатель существует, но incoming == null, исключение возникнет уже внутри добавления. Подробное сообщение помогает локализовать разыменование, но не заменяет проверку входных данных. Описание NullPointerException.
Для HashMap результат get == null неоднозначен: ключ может отсутствовать либо быть явно связан с null. containsKey различает эти ситуации. Ещё одна возможная причина — несовпадение ключей: регистр, пробелы или изменившийся equals/hashCode. Чтение значений Map.
Воспроизведите именно эту ошибку
Сохраните MissingBucket.java; команды: javac -encoding UTF-8 --release 25 MissingBucket.java, затем java MissingBucket.
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MissingBucket {
public static void main(String[] args) {
Map<String, List<String>> groups = new HashMap<>();
try {
groups.get("java").addAll(List.of("Mira"));
} catch (NullPointerException error) {
System.out.println(error.getMessage());
}
System.out.println(groups.containsKey("java"));
}
}На используемой HotSpot первая строка содержит приведённое в начале сообщение, вторая — false. Формулировка диагностического текста зависит от реализации и настроек JVM; устойчивый факт — отсутствующий получатель addAll. Перехват здесь нужен только для демонстрации. Возвращать пустой успешный ответ после такой ошибки в рабочем приложении означало бы скрыть потерянное добавление.
Когда computeIfAbsent действительно подходит
Если по договорённости у каждой группы хранится изменяемый список, можно создавать его при первом обращении. computeIfAbsent вычисляет значение при отсутствии ключа или при текущем null, записывает ненулевой результат и возвращает его. Если функция вернула null, нового списка не будет. Сам метод не обещает атомарность для произвольной Map. Контракт Map.computeIfAbsent).
В файле AppendGroups.java сравним этот вариант с копированием уже существующего значения. Запись ignored -> new ArrayList<>() задаёт функцию создания: её параметром будет ключ, а результатом — новый список. Имя ignored показывает, что для создания пустого списка сам ключ не используется.
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public class AppendGroups {
static void appendMutable(Map<String, List<String>> groups,
String key, Collection<String> incoming) {
Objects.requireNonNull(incoming, "incoming");
groups.computeIfAbsent(key, ignored -> new ArrayList<>())
.addAll(incoming);
}
static void appendCopy(Map<String, List<String>> groups,
String key, Collection<String> incoming) {
Objects.requireNonNull(incoming, "incoming");
groups.compute(key, (ignored, previous) -> {
List<String> next = previous == null
? new ArrayList<>() : new ArrayList<>(previous);
next.addAll(incoming);
return next;
});
}
public static void main(String[] args) {
Map<String, List<String>> groups = new HashMap<>();
appendMutable(groups, "java", List.of("Mira", "Lev"));
appendMutable(groups, "java", List.of("Nina"));
System.out.println(groups.get("java"));
groups.put("nullable", null);
appendMutable(groups, "nullable", List.of("Ada"));
System.out.println(groups.get("nullable"));
groups.put("fixed", List.of("Old"));
try {
appendMutable(groups, "fixed", List.of("New"));
} catch (UnsupportedOperationException expected) {
System.out.println("fixed value cannot be changed");
}
appendCopy(groups, "fixed", List.of("New"));
System.out.println(groups.get("fixed"));
}
}После обычной сборки и запуска AppendGroups вывод:
[Mira, Lev, Nina]
[Ada]
fixed value cannot be changed
[Old, New]Наш первый метод годится для заранее согласованной модели изменяемых значений. Второй создаёт новый список и заменяет ссылку в Map; читатель, который сохранил старую ссылку, продолжит видеть старое содержимое. Это уже другая семантика обновления, поэтому выбирать её нужно явно. Операции вычисления Map.
Четыре исправления, которые не решают одну и ту же проблему
getOrDefault(key, new ArrayList<>()).addAll(...)изменит временный список при отсутствующем ключе, но не добавит его в Map.getOrDefault(key, List.of()).addAll(...)дополнительно столкнётся с немодифицируемым пустым списком.computeIfAbsentне заменяет существующий ненулевойList.of(...); в таком списке добавление запрещено.- Если сама Map получена через
Map.of, операция записи тоже недопустима. Нужен изменяемый контейнер либо новый результат целиком.
При проектировании модели лучше запретить null-значения на границе и определить, означает ли отсутствующий ключ новую группу или ошибку запроса. Автоматическое создание подходит не всегда: опечатка в идентификаторе существующего отдела не должна молча создавать новый отдел.
Отдельно проверьте конкурентность. Замена HashMap на ConcurrentHashMap защищает операции самой карты, но не превращает помещённый внутрь ArrayList в потокобезопасный список. Для этого урока предполагается один поток; многопоточный протокол требует самостоятельного решения. ConcurrentHashMap.
Проверьте контракт добавления
Повторите пример для нового ключа, существующего пустого списка, явного null и значения List.of("Old"). Добавьте вход incoming == null: ожидайте немедленный отказ до изменения карты. Затем сохраните ссылку на старый список и выполните appendCopy; объясните, почему она не изменилась. Проверки должны подтверждать не только отсутствие исключения, но и то, что новые элементы действительно доступны через исходную Map.
Частые вопросы
Можно ли убрать ошибку проверкой if (list != null)?
Можно избежать разыменования, но добавление при отсутствующем списке тогда просто не произойдёт. Сначала определите бизнес-правило: создать группу, сообщить об ошибке или осознанно пропустить запись, сохранив причину.
Почему после исправления появляется UnsupportedOperationException?
Значение уже существует, поэтому функция создания не вызывается. Оно может быть немодифицируемым. Проверьте происхождение списка и самой карты; обработка null и разрешение изменения — независимые свойства.