Map
Определение и контракт
Map<K,V> хранит соответствия «ключ — значение». Каждый ключ встречается не более одного раза; новое значение для того же ключа заменяет старое. Map входит в Collections Framework, но не наследует Collection.
Map<String, Integer> attempts = new HashMap<>();
attempts.put("job-42", 1);
attempts.put("job-42", 2);
int current = attempts.get("job-42"); // 2Основные реализации
| Реализация | Порядок ключей | Средняя стоимость поиска | Особенность |
|---|---|---|---|
HashMap | не гарантирован | O(1) | зависит от equals и hashCode |
LinkedHashMap | вставка или доступ | O(1) | удобно для предсказуемого обхода и LRU-подобных структур |
TreeMap | сортировка | O(log n) | навигация по диапазонам ключей |
EnumMap | порядок enum | близко к O(1) | компактна для ключей одного enum |
ConcurrentHashMap | не гарантирован | в среднем O(1) | конкурентный доступ без общего глобального lock |
Взгляды на карту
keySet, values и entrySet — представления, связанные с исходной картой:
for (Map.Entry<String, Integer> entry : attempts.entrySet()) {
System.out.println(entry.getKey() + "=" + entry.getValue());
}Для обхода пар entrySet лучше, чем повторный get по каждому ключу: ключ и значение уже доступны в одной записи. Удаление через итератор представления удаляет запись из карты.
Отсутствие значения и null
get(key) не различает «ключ отсутствует» и «ключ присутствует со значением null». Если null разрешён реализацией и важен, проверяют containsKey.
if (cache.containsKey(id)) {
return cache.get(id); // значение может быть null
}HashMap допускает один null-ключ и null-значения. ConcurrentHashMap, Map.of и Map.copyOf не допускают null.
Вычисление значений
Map<String, List<String>> byTeam = new HashMap<>();
byTeam.computeIfAbsent("backend", key -> new ArrayList<>())
.add("Alice");
Map<String, Integer> counts = new HashMap<>();
counts.merge("ERROR", 1, Integer::sum);putIfAbsentзаписывает значение, если нет текущего ненулевого значения.computeIfAbsentлениво вычисляет значение.compute,computeIfPresentиmergeобновляют запись через функцию.
Функции этих методов не должны неожиданно менять ту же карту: поведение зависит от реализации, а рассуждать о коде становится трудно.
Ключи как инвариант
Ключ хеш-карты должен иметь стабильные equals и hashCode на всё время хранения:
record OrderId(long value) {}
Map<OrderId, String> states = new HashMap<>();
states.put(new OrderId(7), "READY");Изменяемый ключ может оказаться в корзине, которая больше не соответствует его новому хешу. Для TreeMap аналогичную роль играет стабильный порядок Comparator/Comparable.
Ловушки
- Полагаться на порядок
HashMap. - Использовать изменяемый объект как ключ.
- Путать отсутствие ключа со значением
null. - Выполнять
containsKey+putкак атомарную операцию в многопоточном коде. - Считать
computeIfAbsentуниверсальной защитой от повторного вычисления без учёта контракта конкретной concurrent-карты. - Обходить карту через
keySetиget, когда нужна каждая пара.
Вопросы на собеседовании
- Почему
Mapне являетсяCollection? - Чем отличаются
HashMap,LinkedHashMapиTreeMap? - Что возвращает
put, если ключ уже существовал? - Как отличить отсутствующий ключ от ключа со значением
null? - Почему ключ
HashMapлучше делать неизменяемым? - Для чего нужны
computeIfAbsentиmerge?
Связанные заметки
Иерархия коллекций · HashMap · ConcurrentHashMap · Set · Equals и hashCode · Comparator Java