W3docs

Интерфейс Map в Java

Отображения ключ-значение в Java: интерфейс Map — put, get, remove, keySet, values, entrySet и методы Java 8.

В этой главе рассматривается контракт Map: семь основных методов, три представления для итерации, методы по умолчанию Java 8, делающие работу с картами лаконичной, правила обработки null в разных реализациях и сравнение карт на равенство. По окончании вы будете знать, какие идиомы использовать и какая стандартная реализация подходит для конкретной задачи.

Map<K, V> — вторая половина фреймворка коллекций. В отличие от интерфейса Collection, он не расширяет Collection — это отдельная иерархия, поскольку хранение ключей, связанных со значениями, является иной абстракцией, нежели хранение набора элементов. Внутренне большинство реализаций Set представляют собой просто Map, где значение игнорируется, поэтому в некотором смысле Map — это основная структура, а Set — её более простой аналог.

Контракт краток: каждому ключу сопоставляется не более одного значения, ключи образуют множество (без дубликатов), а значения — произвольную коллекцию (дубликаты значений допустимы). Различие между реализациями заключается в порядке итерации, обработке null, инвариантах порядка и потокобезопасности — но семь основных методов ниже ведут себя одинаково во всех из них.

Семь основных методов

V put(K key, V value);          // insert or overwrite; returns previous value or null
V get(Object key);              // lookup; returns null if missing
V remove(Object key);           // delete; returns previous value or null
boolean containsKey(Object k);  // does the key exist (even if value is null)?
boolean containsValue(Object v); // O(n) scan of values
int size();
boolean isEmpty();

Несколько тонкостей, которые стоит усвоить:

  • put возвращает предыдущее значение для данного ключа или null, если сопоставления не было. Именно так реализуются идиомы «вставить, если отсутствует» — хотя в этом нет необходимости, поскольку putIfAbsent делает именно это и более понятен.

  • Возвращение get значения null означает либо «ключа нет», либо «ключ есть, но его значение равно null». Это неоднозначность, если карта допускает null-значения; используйте containsKey для разграничения или — ещё лучше — getOrDefault для задания значения по умолчанию:

    int count = counts.getOrDefault("java", 0); // 0 if absent

Три представления

Map не является итерируемым напрямую. Для итерации нужно запросить одно из трёх представлений его содержимого:

Set<K>          keys    = map.keySet();
Collection<V>   values  = map.values();
Set<Map.Entry<K, V>> es = map.entrySet();

Эти представления живые — они отражают изменения в базовой карте, и изменения через представление распространяются обратно. Удаление записи через entrySet() удаляет её из карты; итерация по keySet() с вызовом iterator.remove() удаляет запись. Добавить в keySet или values нельзя (нет пары — значения или ключа), но можно очищать или удалять элементы.

Для итерации почти всегда используется entrySet() — получение обеих частей каждой пары одновременно дешевле, чем вызов get(k) для каждого ключа:

for (Map.Entry<String, Integer> e : counts.entrySet()) {
  System.out.println(e.getKey() + " -> " + e.getValue());
}

Или лямбда-форма, добавленная в Java 8:

counts.forEach((k, v) -> System.out.println(k + " -> " + v));

Методы по умолчанию Java 8, которые действительно важны

Java 8 добавила несколько методов Map, принимающих функцию и работающих атомарно. Они превращают многие трёхстрочные паттерны в однострочники:

  • getOrDefault(k, def) — как get(k), но возвращает def вместо null.
  • putIfAbsent(k, v) — выполняет put только если ключ отсутствует.
  • computeIfAbsent(k, fn) — атомарно вычисляет значение при его отсутствии, сохраняет и возвращает его. Краеугольный камень паттерна «мемоизировать дорогостоящий вызов»:
    Map<String, List<Order>> byUser = new HashMap<>();
    byUser.computeIfAbsent(order.user(), u -> new ArrayList<>()).add(order);
  • computeIfPresent(k, biFn) — пересчитывает значение только если ключ уже существует. Полезно для счётчиков, которые должны игнорировать неизвестные ключи.
  • compute(k, biFn) — универсальный: передаёт текущее значение (или null), получает новое. Удаляет запись, если функция возвращает null.
  • merge(k, v, biFn) — объединяет новое значение с существующим, если оно есть. Классический счётчик:
    for (String w : words) {
      counts.merge(w, 1, Integer::sum);   // first time: stores 1; subsequent: adds
    }

Именно эти операции делают современную работу с картами в Java лаконичной. Используйте их вместо пар get/put.

Null-ключи и null-значения

Правила зависят от реализации:

Классnull-ключnull-значение
HashMapодин допустимдопустимы
LinkedHashMapодин допустимдопустимы
TreeMapнетдопустимы
Hashtableнетнет
ConcurrentHashMapнетнет
Map.of(...) (неизменяемый)нетнет

Общее правило для нового кода: не храните null в карте. Используйте Optional, сигнальное значение или просто не добавляйте запись. Фабрика Map.of принудительно соблюдает это правило.

Равенство между реализациями

Два объекта Map считаются equals, если их entrySet() равны — одинаковые ключи, одинаковые значения, независимо от порядка итерации или реализации. HashMap и TreeMap с одинаковыми парами ключ-значение будут равны. Это то же правило «структурного равенства», которому следует Set.

Стандартные реализации — обзор

КлассБазовая структураПорядок итерацииПрименение
HashMapхеш-таблицане определёнпо умолчанию
LinkedHashMapхеш-таблица + связный списокпорядок вставки или доступаLRU-кэши, предсказуемая итерация
TreeMapкрасно-чёрное деревосортировка по ключудиапазонные запросы по ключам, сортированный вывод
Hashtableхеш-таблица, синхронизированнаяне определёнустаревший; редко подходит
ConcurrentHashMapполосатая хеш-таблицане определёнмногопоточный код
EnumMapбитовый массивпорядок enumMap<MyEnum, V>
Map.of(...)неизменяемыйне определённебольшие фиксированные карты

Следующие главы подробно рассматривают повседневные варианты: HashMap, LinkedHashMap и TreeMap. ConcurrentHashMap и EnumMap рассматриваются в более поздних разделах.

Практический пример: счётчики, группировка и три представления

Программа ниже демонстрирует современные идиомы работы с картами: merge для подсчёта, computeIfAbsent для группировки, все три представления и различие между getOrDefault и get.

java— editable, runs on the server

Что можно вынести из запуска:

  • merge(word, 1, Integer::sum) — современный идиоматичный подсчёт слов. Никаких get/put/проверок на null.
  • computeIfAbsent создаёт пустой список ровно один раз для каждого ключа — чистый способ построить Map<K, List<V>> без повсеместных if (m.get(k) == null) m.put(k, new ArrayList<>()).
  • Три представления — это живые окна в одну и ту же карту; entrySet() — самый дешёвый способ итерации, когда нужны обе части каждой пары.
  • getOrDefault устраняет наиболее распространённую причину проверять на null. Используйте его всякий раз, когда есть разумное значение по умолчанию.
  • HashMap и TreeMap с одинаковыми записями equals между собой; единственное, что меняется, — это порядок итерации.

Что дальше

Реализация по умолчанию — и та, которую вы увидите в 90% кода на Java, — основана на хеш-таблице. HashMap — следующая глава; мы рассмотрим массив бакетов, оптимизацию древовидной структуры в Java 8 и что делать, когда ключами являются ваши собственные классы.

Практика

Практика
`counts` — это `HashMap<String, Integer>`. Какая строка является идиоматичным способом увеличить счётчик для `'java'`, считая отсутствующий ключ начинающимся с нуля?
`counts` — это `HashMap<String, Integer>`. Какая строка является идиоматичным способом увеличить счётчик для `'java'`, считая отсутствующий ключ начинающимся с нуля?
Was this page helpful?