Интерфейс Map в Java: методы, Map.Entry и примеры
Счётчик слов из трёх строк выглядит безобидно: counts.put(w, counts.get(w) + 1). Но на первом же слове программа падает с NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is null — для нового ключа get() возвращает null, а распаковать null в int нельзя. С Java 8 та же задача решается одной строкой counts.merge(w, 1, Integer::sum), и это лишь один из методов, о которых многие не знают.
Интерфейс Map (java.util.Map<K, V>) — это коллекция пар «ключ–значение», в которой каждый ключ встречается не больше одного раза, а значение находят по ключу. Map также называют отображением, картой, словарём или просто «мапой». Основные реализации — HashMap, LinkedHashMap и TreeMap; они отличаются порядком ключей и скоростью операций.
Что такое Map в Java
Map в Java — интерфейс для хранения пар «ключ–значение»: ключи уникальны, значения могут повторяться, а доступ к значению идёт по ключу, а не по индексу. Map входит в Collection Framework, но интерфейс Collection не наследует — это отдельная ветка иерархии со своими методами.
Интерфейс обобщённый и объявлен так:
public interface Map<K, V> Первый параметр K — тип ключей, второй V — тип значений. Например, Map<String, Integer> хранит зарплату по фамилии, а Map<Long, Person> — человека по идентификатору. Ключи и значения — только объекты: примитивы автоматически упаковываются в Integer, Long и другие обёртки.
import java.util.HashMap;
import java.util.Map;
public class MapExample {
public static void main(String[] args) {
Map<String, Integer> salaries = new HashMap<>();
salaries.put("Иванов", 3400);
salaries.put("Петров", 1200);
salaries.put("Сидоров", 1300);
System.out.println(salaries.get("Иванов"));
System.out.println(salaries);
}
} Вывод:
3400
{Иванов=3400, Сидоров=1300, Петров=1200} Порядок вывода не совпадает с порядком добавления: HashMap раскладывает ключи по бакетам хеш-таблицы. Как именно, разобрано в уроке HashMap: как устроена внутри.
Как проверяется уникальность ключа, зависит от реализации: HashMap и LinkedHashMap сравнивают ключи через hashCode() и equals(), TreeMap — через compareTo() или Comparator, а IdentityHashMap — оператором ==. Разрешены ли null-ключи и null-значения, тоже решает реализация (см. таблицу реализаций).
Место Map в Collection Framework показано на схеме:

Интерфейс Map расширяют SortedMap и NavigableMap (их методы — в уроке TreeMap, SortedMap и NavigableMap), а с Java 21 — ещё SequencedMap. Общая схема коллекций и ответ на вопрос, почему Map не наследует Collection, — в уроке Коллекции в Java.
Основные методы Map: put, get, remove
Основные методы Map: put() добавляет или заменяет пару, get() возвращает значение по ключу, remove() удаляет пару, containsKey() и containsValue() проверяют наличие, size() и isEmpty() сообщают о размере. Метод put() для уже существующего ключа перезаписывает значение и возвращает старое.
| Метод | Что делает | Граничный случай |
|---|---|---|
V put(K key, V value) | Добавляет пару; если ключ уже есть — заменяет значение | Возвращает старое значение или null, если ключа не было |
V get(Object key) | Возвращает значение по ключу | Ключа нет — null; тот же null будет и для ключа со значением null |
boolean containsKey(Object key) | Проверяет, есть ли ключ | Единственный надёжный способ отличить «ключа нет» от «значение null» |
boolean containsValue(Object value) | Проверяет, есть ли значение | Перебирает все пары — O(n) даже у HashMap |
V remove(Object key) | Удаляет пару по ключу | Возвращает удалённое значение или null, исключения нет |
V replace(K key, V value) | Заменяет значение, только если ключ уже есть (Java 8) | Ключа нет — ничего не добавляет и возвращает null |
void putAll(Map<? extends K, ? extends V> m) | Копирует все пары из m | Совпадающие ключи перезаписываются значениями из m |
int size(), boolean isEmpty() | Число пар / пуста ли карта | Считаются пары, а не отдельные ключи и значения |
void clear() | Удаляет все пары | У Map.of() — UnsupportedOperationException |
Все эти методы в одном примере:
import java.util.HashMap;
import java.util.Map;
public class MapMethodsExample {
public static void main(String[] args) {
Map<String, Integer> salaries = new HashMap<>();
System.out.println(salaries.isEmpty());
System.out.println(salaries.put("Иванов", 3400));
salaries.put("Петров", 1200);
salaries.put("Сидоров", 1300);
System.out.println(salaries.put("Иванов", 3500));
System.out.println(salaries.get("Петров"));
System.out.println(salaries.get("Харитонов"));
System.out.println(salaries.containsKey("Сидоров"));
System.out.println(salaries.containsValue(1200));
System.out.println(salaries.containsValue(1200L));
System.out.println(salaries.replace("Харитонов", 900));
System.out.println(salaries.remove("Петров"));
System.out.println("Количество элементов: " + salaries.size());
System.out.println(salaries);
}
} Вывод:
true
null
3400
1200
null
true
true
false
null
1200
Количество элементов: 2
{Иванов=3500, Сидоров=1300} Первый put("Иванов", ...) вернул null — ключа ещё не было, второй вернул старую зарплату 3400 и заменил её на 3500. replace() для отсутствующего Харитонова ничего не добавил. Неочевидная строка — containsValue(1200L): литерал 1200L упаковывается в Long, а Long никогда не равен Integer по equals(), поэтому ответ false. Компилятор такую ошибку не поймает: параметр метода имеет тип Object.
Map.Entry, keySet(), values() и entrySet()
Map.Entry<K, V> — вложенный интерфейс, который описывает одну пару карты: методы getKey(), getValue() и setValue(). Метод entrySet() возвращает множество таких пар, keySet() — множество ключей, values() — коллекцию значений. Все три — представления (views) самой карты, а не копии.
| Метод | Возвращает | Что можно делать | Нюанс |
|---|---|---|---|
Set<K> keySet() | Множество ключей | Перебирать, удалять (remove, removeIf, retainAll) | add() — UnsupportedOperationException: значения для нового ключа нет |
Collection<V> values() | Коллекцию значений | Перебирать, удалять пары по значению | Тип Collection, а не Set: значения могут повторяться |
Set<Map.Entry<K, V>> entrySet() | Множество пар | Перебирать, удалять, менять значение через entry.setValue() | Самый быстрый способ обойти ключи и значения вместе |
import java.util.HashMap;
import java.util.Map;
public class EntrySetExample {
public static void main(String[] args) {
Map<String, Integer> salaries = new HashMap<>();
salaries.put("Иванов", 3400);
salaries.put("Петров", 1200);
salaries.put("Сидоров", 1300);
System.out.println("Ключи: " + salaries.keySet());
System.out.println("Значения: " + salaries.values());
for (Map.Entry<String, Integer> entry : salaries.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
entry.setValue(entry.getValue() + 100); // меняет саму карту
}
System.out.println(salaries);
salaries.keySet().remove("Петров"); // удаляет пару из карты
System.out.println(salaries);
}
} Вывод:
Ключи: [Иванов, Сидоров, Петров]
Значения: [3400, 1300, 1200]
Иванов: 3400
Сидоров: 1300
Петров: 1200
{Иванов=3500, Сидоров=1400, Петров=1300}
{Иванов=3500, Сидоров=1400} Изменения через представления сразу попадают в карту и обратно: после keySet().remove("Петров") пары с Петровым нет и в salaries. Подробнее о коллекциях-представлениях — в уроке Backed Collections. Получить отдельную пару без карты можно через Map.entry("Иванов", 3400) (Java 9, неизменяемая) или new AbstractMap.SimpleEntry<>("Иванов", 3400) (изменяемая).
Как перебрать Map
Перебрать Map в Java можно тремя способами: циклом for-each по entrySet(), когда нужны и ключ, и значение; по keySet() или values(), когда нужно что-то одно; и методом forEach((k, v) -> ...) из Java 8. Удалять пары во время обхода нужно через removeIf() или итератор, а не через map.remove().
Map<String, Integer> salaries = new HashMap<>();
salaries.put("Иванов", 3400);
salaries.put("Петров", 1200);
salaries.put("Сидоров", 1300);
salaries.forEach((name, salary) -> System.out.println(name + " = " + salary));
salaries.entrySet().removeIf(e -> e.getValue() < 1250);
System.out.println(salaries); Вывод:
Иванов = 3400
Сидоров = 1300
Петров = 1200
{Иванов=3400, Сидоров=1300} Обход keySet() с последующим get(key) внутри цикла работает, но ищет каждый ключ повторно — entrySet() и forEach() отдают пару сразу. Если же вызвать salaries.remove(key) внутри for-each, следующий шаг итератора, как правило, бросит ConcurrentModificationException. Почему так происходит, объясняет урок Iterator; безопасные варианты — entrySet().removeIf(), keySet().removeIf(), values().removeIf() или iterator.remove().
Методы Map в Java 8: merge, compute, getOrDefault
В Java 8 в интерфейс Map добавили default-методы, которые заменяют типичную связку «проверить ключ — прочитать — записать» одним вызовом: getOrDefault(), putIfAbsent(), computeIfAbsent(), computeIfPresent(), compute(), merge(), replaceAll() и forEach(). Если функция в compute* или merge возвращает null, пара удаляется.
| Метод | Что делает | Неочевидное поведение |
|---|---|---|
getOrDefault(key, defaultValue) | Значение по ключу или defaultValue, если ключа нет | Для ключа со значением null вернёт null, а не defaultValue |
putIfAbsent(key, value) | Добавляет пару, только если ключа нет | Возвращает текущее значение; ключ со значением null считается отсутствующим |
computeIfAbsent(key, k -> value) | Если ключа нет — вычисляет значение, кладёт и возвращает его | Функция вызывается только при отсутствии ключа — удобно для new ArrayList<>() |
computeIfPresent(key, (k, v) -> newV) | Если ключ есть — пересчитывает значение | Функция вернула null — пара удаляется |
compute(key, (k, v) -> newV) | Пересчитывает значение в любом случае; v == null, если ключа нет | Нужно самому обработать v == null, иначе NullPointerException |
merge(key, value, (old, v) -> newV) | Ключа нет — кладёт value; есть — объединяет старое и новое значение | value == null — NullPointerException; функция вернула null — пара удаляется |
replace(key, value), replace(key, oldValue, newValue) | Заменяет значение существующего ключа; второй вариант — только при совпадении старого значения | Новых ключей не добавляет |
replaceAll((k, v) -> newV) | Пересчитывает все значения | Ключи не меняются |
remove(key, value) | Удаляет пару, только если ключу сопоставлено именно это значение | Возвращает boolean, а не значение |
forEach((k, v) -> ...) | Выполняет действие для каждой пары | Изменение структуры карты внутри — ConcurrentModificationException |
Как эти методы ведут себя на одной карте:
import java.util.HashMap;
import java.util.Map;
public class Java8MapMethods {
public static void main(String[] args) {
Map<String, Integer> stock = new HashMap<>();
stock.put("apple", 5);
System.out.println(stock.getOrDefault("pear", 0));
System.out.println(stock.putIfAbsent("apple", 10));
System.out.println(stock.putIfAbsent("pear", 3));
System.out.println(stock.computeIfPresent("apple", (k, v) -> v - 5));
System.out.println(stock.compute("kiwi", (k, v) -> v == null ? 1 : v + 1));
System.out.println(stock.merge("pear", 2, Integer::sum));
System.out.println(stock.computeIfPresent("apple", (k, v) -> v == 0 ? null : v));
System.out.println(stock);
}
} Вывод:
0
5
null
0
1
5
null
{kiwi=1, pear=5} putIfAbsent("apple", 10) вернул текущие 5 и ничего не изменил. merge("pear", 2, Integer::sum) сложил 3 и 2. Последний computeIfPresent вернул null — и пара apple исчезла из карты: так удобно удалять счётчики, дошедшие до нуля.
getOrDefault не спасает от null-значения
Метод возвращает defaultValue, только если ключа нет совсем. После phones.put("Анна", null) вызов phones.getOrDefault("Анна", "нет номера") вернёт null, а phones.getOrDefault("Олег", "нет номера") — строку «нет номера». Если null в значениях возможен, пишите Objects.requireNonNullElse(map.get(key), defaultValue) (Java 9) или не храните null в карте вовсе.
Подсчёт частоты слов через merge()
Вот исправленный счётчик из начала урока. merge() кладёт 1 для нового слова и прибавляет 1 к уже существующему — без get(), проверок и NullPointerException:
Map<String, Integer> counts = new HashMap<>();
for (String w : "to be or not to be".split(" ")) {
counts.merge(w, 1, Integer::sum);
}
System.out.println(counts); Вывод:
{not=1, be=2, or=1, to=2} Группировка значений через computeIfAbsent()
Когда значением карты служит список, computeIfAbsent() создаёт его при первом обращении к ключу и сразу возвращает — в него можно добавлять элемент в той же строке:
Map<Integer, List<String>> byLength = new HashMap<>();
for (String name : List.of("Аня", "Борис", "Ира", "Олег", "Вера")) {
byLength.computeIfAbsent(name.length(), k -> new ArrayList<>()).add(name);
}
System.out.println(byLength); Вывод:
{3=[Аня, Ира], 4=[Олег, Вера], 5=[Борис]} С putIfAbsent(key, new ArrayList<>()) код получился бы длиннее: метод возвращает null для нового ключа, и новый список при каждом вызове создаётся впустую, даже если ключ уже есть.
Map.of, Map.ofEntries и Map.copyOf
С Java 9 небольшую неизменяемую карту создают фабричным методом Map.of(k1, v1, k2, v2, ...) — до 10 пар, для большего числа пар есть Map.ofEntries(Map.entry(k, v), ...). Map.copyOf(map) (Java 10) делает неизменяемую копию. Такие карты запрещают null, повторяющиеся ключи и любые изменения.
import java.util.HashMap;
import java.util.Map;
public class MapOfExample {
public static void main(String[] args) {
Map<String, Integer> codes = Map.of("RU", 7, "KZ", 7, "BY", 375);
System.out.println(codes.get("BY"));
try {
codes.put("UA", 380);
} catch (UnsupportedOperationException e) {
System.out.println("put: " + e);
}
try {
Map.of("RU", 7, "RU", 8);
} catch (IllegalArgumentException e) {
System.out.println("Map.of: " + e.getMessage());
}
Map<String, Integer> editable = new HashMap<>(codes);
editable.put("UA", 380);
System.out.println(editable.size());
}
} Вывод:
375
put: java.lang.UnsupportedOperationException
Map.of: duplicate key: RU
4 Map.of("a", null) и codes.get(null) бросают NullPointerException. Порядок обхода карт из Map.of() не определён и может меняться от запуска к запуску, поэтому выводить такую карту целиком в тестах не стоит. Изменяемая карта с начальными данными получается обёрткой new HashMap<>(Map.of(...)). Сравнение всех способов получить неизменяемую коллекцию — в разделе Неизменяемые коллекции.
Как менялся Map по версиям Java
Интерфейс Map появился в Java 1.2 вместе с Collection Framework. Самое большое обновление пришлось на Java 8 (default-методы merge, compute*, getOrDefault), затем добавились фабрики Map.of (Java 9) и Map.copyOf (Java 10). В Java 11 у Map ничего не изменилось, в Java 17 добавился только Map.Entry.copyOf(); следующее крупное изменение — SequencedMap в Java 21.
| Версия | Что изменилось |
|---|---|
| Java 1.2 | Map, Map.Entry, SortedMap, классы HashMap, TreeMap, WeakHashMap; Hashtable переделан под Map |
| Java 1.4 | LinkedHashMap, IdentityHashMap |
| Java 5 | Обобщённый Map<K, V>; ConcurrentHashMap, EnumMap |
| Java 6 | NavigableMap — урок TreeMap и NavigableMap |
| Java 8 | Default-методы getOrDefault, putIfAbsent, compute, computeIfAbsent, computeIfPresent, merge, replace, replaceAll, remove(key, value), forEach; Map.Entry.comparingByKey() и comparingByValue() |
| Java 9 | Map.of(), Map.ofEntries(), Map.entry() |
| Java 10 | Map.copyOf() |
| Java 17 | Map.Entry.copyOf(entry) — неизменяемая копия пары, не связанная с картой |
| Java 19 | HashMap.newHashMap(n), LinkedHashMap.newLinkedHashMap(n) — карта под заданное число элементов без перестройки |
| Java 21 | SequencedMap: firstEntry(), lastEntry(), pollFirstEntry(), reversed() у LinkedHashMap и TreeMap; putFirst()/putLast() работают только у LinkedHashMap (у TreeMap — UnsupportedOperationException) — урок LinkedHashMap |
Полное описание каждого метода — в официальной документации Javadoc интерфейса Map (Java 21).
Реализации Map: какую выбрать
По умолчанию выбирают HashMap: самые быстрые get() и put(), порядок ключей не гарантирован. LinkedHashMap нужен, когда важен порядок вставки, TreeMap — когда ключи должны быть отсортированы, а ConcurrentHashMap — когда к карте обращаются несколько потоков.
| Класс | Порядок ключей | null-ключ / null-значения | Потокобезопасность | get()/put() | Подробнее |
|---|---|---|---|---|---|
HashMap | Не гарантирован | Один null-ключ / да | Нет | O(1) в среднем | HashMap: как устроена |
LinkedHashMap | Порядок вставки (или доступа) | Как у HashMap | Нет | O(1) в среднем | LinkedHashMap и LRU-кэш |
TreeMap | Сортировка по ключу | null-ключ — NullPointerException / да | Нет | O(log n) | TreeMap и NavigableMap |
ConcurrentHashMap | Не гарантирован | Нет / нет — NullPointerException | Да, без общей блокировки | O(1) в среднем | Потокобезопасные коллекции |
Hashtable | Не гарантирован | Нет / нет — NullPointerException | Да, все методы synchronized | O(1) в среднем | Legacy Classes — в новом коде не используют |
EnumMap | Порядок объявления констант enum | Нет / да | Нет | O(1), внутри массив | Ключи — только константы одного enum |
IdentityHashMap | Не гарантирован | Да / да | Нет | O(1) в среднем | Ключи сравниваются через ==, а не equals() |
WeakHashMap | Не гарантирован | Да / да | Нет | O(1) в среднем | Пара исчезает, когда на ключ не осталось обычных ссылок |
IdentityHashMap — единственная стандартная реализация, которая намеренно нарушает общий контракт Map: два ключа new String("id") для неё разные, поэтому после двух put() её size() равен 2, а у HashMap с теми же ключами — 1. Её применяют в служебном коде, где важна именно идентичность объектов: при глубоком копировании графа объектов, сериализации, отслеживании уже обработанных узлов.
Переменную принято объявлять типом интерфейса: Map<String, Integer> map = new HashMap<>();. Тогда смена реализации на LinkedHashMap или TreeMap — это правка одной строки. Тип NavigableMap или SequencedMap указывают, только если нужны их собственные методы.
Требования к ключу Map
Ключ HashMap должен корректно переопределять equals() и hashCode() и не меняться, пока лежит в карте. Поэтому ключами чаще всего делают неизменяемые объекты: String, Integer, enum, record. Если изменить поле, от которого зависит hashCode(), get() по тому же объекту вернёт null, хотя пара всё ещё в карте.
Как именно «теряется» изменённый ключ и почему это связано с бакетами, показано на примере в уроке HashMap: как устроена внутри, а сам контракт equals()/hashCode() — в уроке Set и HashSet. Для TreeMap правило другое: ключ должен быть Comparable или сравниваться переданным Comparator (урок Comparable и Comparator). Частый практический приём — брать в качестве ключа неизменяемое поле объекта-значения: Map<String, Product> с артикулом товара вместо Map<Product, ...>.
Часто задаваемые вопросы
Как отсортировать Map по значениям?
Сама карта по значениям не сортируется — сортируют поток её пар и собирают результат в LinkedHashMap, чтобы сохранить полученный порядок: map.entrySet().stream().sorted(Map.Entry.comparingByValue()).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a, LinkedHashMap::new)). Для убывания — Map.Entry.comparingByValue(Comparator.reverseOrder()). Сортировку по ключам даёт TreeMap.
Как получить ключ по значению в Map?
Прямого метода нет: значения не уникальны, и одному значению может соответствовать несколько ключей. Ключи ищут перебором entrySet(): map.entrySet().stream().filter(e -> e.getValue().equals(value)).map(Map.Entry::getKey).toList(). Это O(n); если поиск по значению нужен часто, держат вторую, обратную карту.
Чем putIfAbsent() отличается от computeIfAbsent()?
putIfAbsent(key, value) получает готовое значение, которое создаётся при каждом вызове, и возвращает прежнее значение (для нового ключа — null). computeIfAbsent(key, k -> ...) вызывает функцию только при отсутствии ключа и возвращает актуальное значение — поэтому его используют для группировки в списки. Пример — в разделе Группировка через computeIfAbsent().
Сохраняет ли HashMap порядок добавления элементов?
Нет: HashMap обходит пары в порядке бакетов хеш-таблицы, и этот порядок может измениться после увеличения таблицы. Порядок вставки гарантирует LinkedHashMap, сортировку ключей — TreeMap. Чем они отличаются внутри, разобрано в уроке LinkedHashMap.
Практика по теме
Закрепите урок на задачах — у каждой есть уровень и оценка времени.
Все задачи модуля →Видео объяснение
Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.
Комментарии