Коллекции ·
‹ Предыдущий Следующий ›
⏱ 5 минут чтения Обновлено: 2026-10-24

Интерфейс 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, HashMap, LinkedHashMap, TreeMap, Hashtable

Интерфейс 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.

Практика по теме

Закрепите урок на задачах — у каждой есть уровень и оценка времени.

Все задачи модуля →

Видео объяснение

Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.

Комментарии

Зарегистрируйтесь или войдите, чтобы иметь возможность оставить комментарий.