Map в Java — это интерфейс из пакета java.util, который описывает контракт коллекции пар «ключ — значение», где каждый ключ уникален и связан ровно с одним значением. Появился в JDK 1.2 как часть Collections Framework. Ниже разберем, чем контракт Map отличается от Collection, какие реализации выбирать (HashMap, LinkedHashMap, TreeMap, Hashtable, ConcurrentHashMap), ключевые методы и почему для ключей критичны equals и hashCode.
Содержание
Map — это не Collection
Важно сразу развести два интерфейса. Collection (и его наследники List, Set, Queue) работает с отдельными элементами. Map работает с парами и в иерархию Collection не входит — это отдельная ветка Collections Framework.
Поэтому у Map нет методов add(element) или iterator(). Вместо этого он дает три представления (view), каждое из которых уже является коллекцией: keySet() (ключи), values() (значения) и entrySet() (пары). Именно их и перебирают.
Минимальный рабочий пример — создать карту, положить пары и прочитать их:
import java.util.HashMap;
import java.util.Map;
public class MapDemo {
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<>();
map.put(1, "Petyr");
map.put(2, "Sansa");
map.put(3, "Jon");
System.out.println(map.get(2)); // Sansa
System.out.println(map.containsKey(4)); // false
System.out.println(map.size()); // 3
}
}
Ключи и значения — только ссылочные типы. Примитив напрямую параметром обобщения (generics) быть не может, поэтому пишут Map<Integer, String>, а не Map<int, String>; автоупаковка (autoboxing) превращает int в Integer при вызове put.
Реализации: когда что брать
Все реализации подчиняются одному контракту Map, поэтому методы у них одинаковые. Различаются они порядком элементов, поведением с null, потокобезопасностью и скоростью. Выбор идет от задачи.
| Реализация | Порядок ключей | null-ключ | Потокобезопасна | get/put (в среднем) | Когда брать |
|---|---|---|---|---|---|
| HashMap | не гарантирован | 1 разрешен | нет | O(1) | вариант по умолчанию |
| LinkedHashMap | порядок вставки или доступа | 1 разрешен | нет | O(1) | нужен предсказуемый обход, кеш по типу LRU |
| TreeMap | сортировка по ключу | запрещен (NPE) | нет | O(log n) | нужен отсортированный обход и запросы по диапазону |
| Hashtable | не гарантирован | запрещен | да (весь объект) | O(1) | не брать в новом коде (legacy) |
| ConcurrentHashMap | не гарантирован | запрещен | да | O(1) | доступ из нескольких потоков |
Пояснения к таблице:
- HashMap разрешает один
null-ключ и любое числоnull-значений, порядок не хранит. Начиная с Java 8 длинная цепочка коллизий в одной корзине превращается из связного списка в сбалансированное дерево (это ускоряет поиск при плохом распределении хешей), но полагаться на конкретный порядок обхода все равно нельзя. - LinkedHashMap наследует HashMap и дополнительно хранит порядок: по умолчанию — порядок вставки, а с конструктором
accessOrder = true— порядок обращения, на чем удобно строить кеш с вытеснением. - TreeMap держит ключи отсортированными (по натуральному порядку или через переданный
Comparator) и реализуетNavigableMap, поэтому умеетfirstKey,floorKey,headMapи подобные запросы.null-ключ он не принимает. - Hashtable синхронизирует каждый метод целиком и не допускает
nullни в ключе, ни в значении. Это класс из JDK 1.0, в новом коде его заменяют наHashMap(однопоток) илиConcurrentHashMap(многопоток). - ConcurrentHashMap дает потокобезопасность без блокировки всей карты и тоже запрещает
null(иначе нельзя отличить «нет ключа» от «значение null» при конкурентном доступе).
Разница в порядке видна на одном наборе ключей:
import java.util.*;
public class OrderDemo {
public static void main(String[] args) {
List<Map<String, Integer>> maps = List.of(
new HashMap<>(), new LinkedHashMap<>(), new TreeMap<>());
for (Map<String, Integer> m : maps) {
m.put("banana", 1);
m.put("apple", 2);
m.put("cherry", 3);
}
System.out.println(maps.get(0).keySet()); // HashMap: порядок не гарантирован
System.out.println(maps.get(1).keySet()); // LinkedHashMap: [banana, apple, cherry]
System.out.println(maps.get(2).keySet()); // TreeMap: [apple, banana, cherry]
}
}
Ключевые методы
put(key, value) кладет пару и возвращает прежнее значение для этого ключа или null, если ключа не было. Повторный put с тем же ключом перезаписывает значение — дубликатов ключей в карте не бывает.
Map<Integer, String> map = new HashMap<>();
System.out.println(map.put(1, "Jon")); // null - ключа еще не было
System.out.println(map.put(1, "Sansa")); // Jon - вернул прежнее значение
System.out.println(map.get(1)); // Sansa - значение перезаписано
Часто нужнее методы, которые аккуратнее работают с отсутствием ключа: getOrDefault возвращает запасное значение вместо null, putIfAbsent не перетирает существующее, а merge удобен для счетчиков и агрегации.
Map<String, Integer> counts = new HashMap<>();
System.out.println(counts.getOrDefault("a", 0)); // 0 - ключа нет, вернул default
counts.merge("a", 1, Integer::sum); // ключа нет -> кладет 1
counts.merge("a", 1, Integer::sum); // ключ есть -> 1 + 1
System.out.println(counts.get("a")); // 2
Остальной базовый набор: get(key) читает значение (или null), remove(key) удаляет пару и возвращает удаленное значение, containsKey и containsValue проверяют наличие, size и isEmpty дают размер, clear очищает карту.
Итерация через entrySet
Для перебора и ключа, и значения одновременно берут entrySet(): это один проход по парам, тогда как связка keySet() + get() внутри цикла делает лишний поиск на каждой итерации.
import java.util.LinkedHashMap;
import java.util.Map;
public class IterationDemo {
public static void main(String[] args) {
Map<Integer, String> map = new LinkedHashMap<>();
map.put(1, "Jon");
map.put(2, "Sansa");
for (Map.Entry<Integer, String> e : map.entrySet()) {
System.out.println(e.getKey() + " -> " + e.getValue());
}
// 1 -> Jon
// 2 -> Sansa
}
}
Тот же обход в стиле Java 8 — через forEach с лямбдой: map.forEach((k, v) -> System.out.println(k + " -> " + v));. Порядок вывода здесь предсказуем, потому что взят LinkedHashMap; у HashMap он был бы не гарантирован.
equals и hashCode для ключей
Это самая частая скрытая ошибка. HashMap сначала по hashCode() ключа выбирает корзину, а затем внутри нее сравнивает ключи через equals(). Если у вашего класса-ключа эти методы не переопределены, действуют версии из Object, которые сравнивают ссылки, а не содержимое. Тогда два «одинаковых» объекта окажутся разными ключами:
import java.util.HashMap;
import java.util.Map;
public class KeyContract {
static class Point {
final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
// equals и hashCode НЕ переопределены
}
public static void main(String[] args) {
Map<Point, String> map = new HashMap<>();
map.put(new Point(1, 2), "A");
System.out.println(map.get(new Point(1, 2))); // null - разные объекты
}
}
Контракт таков: если a.equals(b), то обязательно a.hashCode() == b.hashCode(). Обратное не требуется (равные хеши у неравных объектов — это коллизия, она допустима). Переопределяем оба метода вместе, обычно через Objects:
static class Point {
final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point)) return false;
Point p = (Point) o;
return x == p.x && y == p.y;
}
@Override
public int hashCode() {
return java.util.Objects.hash(x, y);
}
}
С таким классом map.get(new Point(1, 2)) вернет "A". Отдельная рекомендация — делать поля ключа неизменяемыми (final): если после вставки поменять поле, участвующее в hashCode, объект «потеряется» в своей корзине, и найти его по этому же ключу уже не получится.
Неизменяемые карты (Java 9+)
С Java 9 есть фабричные методы для готовых неизменяемых карт: Map.of(...) для нескольких пар и Map.ofEntries(...) для произвольного числа. Такую карту нельзя менять — любые put, remove, clear бросают исключение.
import java.util.Map;
public class ImmutableDemo {
public static void main(String[] args) {
Map<String, Integer> ages = Map.of("Jon", 24, "Sansa", 20);
System.out.println(ages.get("Jon")); // 24
ages.put("Arya", 18); // UnsupportedOperationException
}
}
Выводы
Map— интерфейс пар «ключ — значение» с уникальными ключами; он не наследуетCollection, а дает представленияkeySet,values,entrySet.- Выбор реализации идет от задачи:
HashMapпо умолчанию,LinkedHashMapза предсказуемый порядок,TreeMapза сортировку и запросы по диапазону,ConcurrentHashMapза многопоточность;Hashtableв новом коде не нужен. - Поведение с
nullразличается:HashMap/LinkedHashMapдопускаютnull-ключ, аTreeMap,HashtableиConcurrentHashMap— нет. - Для перебора пар берите
entrySet()— это один проход вместоkeySet()+get(). - Для класса-ключа переопределяйте
equalsиhashCodeсогласованно, а поля ключа держите неизменяемыми.
Где применяется / связь с практикой
Map — базовый инструмент почти в любом Java-приложении: кеши, подсчет частот, индексация по идентификатору, конфигурации, группировка данных. Умение выбрать реализацию под задачу и корректно оформить класс-ключ — то, что отличает рабочий код от кода, который «иногда теряет данные».
Освойте тему на практике
Если хочется собрать эти основы системно и с практикой, посмотрите курс Java Basic: там коллекции разбирают на задачах. Проверить формат и уровень перед стартом удобно на открытых уроках — это бесплатные занятия с разбором тем.
Смежные темы: Интерфейсы и коллекции в Java, Java: что это за язык и зачем он нужен.
FAQ
Чем HashMap отличается от HashSet?
HashSet хранит только элементы (множество без дубликатов) и внутри реализован поверх HashMap, где ваши элементы — это ключи, а значением служит один общий объект-пустышка. То есть Set — это про наличие, Map — про связь ключа со значением.
Можно ли в HashMap положить null как значение и как ключ?
Да: HashMap допускает один null-ключ и сколько угодно null-значений. Из-за этого get может вернуть null и когда ключа нет, и когда значение равно null; различить случаи помогает containsKey.
Что будет, если менять поля объекта-ключа после вставки?
Если изменить поле, участвующее в hashCode, объект окажется «не в той» корзине, и поиск по нему сломается: get/containsKey вернут так, будто ключа нет. Поэтому ключи делают неизменяемыми.



