Map. Первое знакомство
Дорогу осилит идущийВ рамках текущего урока мы познакомимся с последним из типов коллекций — Map (словарь, сленг. — мапа). Разберем методы интерфейсов, посмотрим на реализации. Но знакомство с внутренним устройством этих реализаций будет вынесено в отдельный урок — эта тема популярна на собеседованиях и стоит разобраться с ней подробно.
В целом, структура урока будет симметрична таковой в «Урок 41. Set. Первое знакомство».
Общая информация
Все реализации Map являются ни чем иным, как ассоциативным массивом. Соответственно, любая мапа представляет собой набор пар вида «ключ-значение», где ключ уникален, значение — нет.
Таким образом, если все изученные нами типы коллекций имели параметризацию вида <E>, то Map имеет параметризацию вида <K, V> (key, value).
Именно поэтому Map, в отличии от остальных типов коллекций, не является наследником Collection и Iterable. Строго говоря, интерфейс Map вообще ни от кого не наследуется. Поэтому ряд методов, уже привычных по остальным коллекциям, может отсутствовать.
С точки зрения применения, Map тяжело переоценить: от обычного хранения некоего множества данных с доступом к элементам по ключу до реализации более сложных структур на основании коллекций (мапа мап, мапа сетов и пр.) и коллекции, хранящей разного рода агрегаты над первичными данными.
Также реализации Map лежат в основе уже изученных реализаций Set'а.
Интерфейсы Map, Map.Entry
Основа иерархии Map — интерфейс Map. Его методы во многом схожи с методами Collection или какого-нибудь из его наследников, некоторые — с поправкой на особенности параметризации.
В свою очередь Entry — вложенный интерфейс, является основой единичного значения Map — пары «ключ-значение». Грубо говоря, любая мапа — набор элементов типа Entry.
Более подробно с методами этих интерфейсов предлагаю ознакомиться на основании статьи (до пункта «Классы отображений. HashMap»): https://metanit.com/java/tutorial/5.8.php
Кроме методов Map, описанных в статье, стоит также выделить несколько методов (групп методов), которые были опущены:
- Методы, параметры которых являются функциональными интерфейсами. После темы коллекций мы познакомимся с ФП в Java, в т. ч. вернемся к этим методам. В Collection их было меньше, у Map — больше. В любом случае, работать с коллекциями можно и без них;
- Методы replace(). Заменяют существующее значение для указанного ключа на новое. Существуют две перегруженные версии — заменяющая значение, если существует ключ, а также заменяющая значение по ключу, лишь если старое значение совпадает с указанным в соответствующем параметре.
- Статические методы создания мапы (или Entry). Нечто похожее мы видели в List и Set. Подробнее разберем эти методы ниже.
Статические методы в Map:
- of(). В отличие от List и Set, в Map эти методы принимают параметрами ключи и значения — каждый нечетный параметр будет key, следующий за ним четный — value. Существуют реализации of() от одной до десяти пар «ключ-значение». Перегруженная версия этого метода для varargs отсутствует в силу особенностей параметризации. Также обратите внимание: передача в такие методы null приведет к исключению.
- Вместо of() с varargs есть метод ofEntries(), принимающий массив Map.Entry (в виде varargs) и формирующий на их основании мапу. Не самая удобная, но альтернатива. Также, как и в of(), null-значения недопустимы. Ни в качестве параметров (Entry), ни в качестве ключей или значений этих Entry;
- copyOf() также существует, но, в отличии от известных нам реализаций, принимает параметром не Collection, а Map. Логичный, но, почему-то не для всех очевидный нюанс;
- И, наконец, не имеющий аналогов метод entry(). Создает неизменяемый объект типа Map.Entry на основании переданных параметров ключа и значения.
Для Entry в статье почему-то были опущены статические методы comparingByKey() и comparingByValue(). Каждый в двух реализациях. Возвращают компараторы для сравнения Entry по ключу или значению соответственно. Как в естественном порядке (в соответсвии с имплементацией Comparable, если она есть), так и на основании переданного параметром компаратора.
Также был опущен статический метод copyOf(), создающий новое Entry на основании переданного параметром.
Работа с Entry напрямую — не самая лучшая практика, особенно за пределами Stream API (с ним мы познакомимся уже достаточно скоро). Но иногда она является меньшим из зол. Поэтому лучше иметь общее представление о содержимом этого интерфейса.
Интерфейс SortedMap
Полагаю, на этом этапе вы уже догадываетесь, что иерархия наследования Map симметрична таковой у Set. Что, в целом, логично, учитывая, что набор ключей мапы — то, на чем и строится основное взаимодействие с этим типом коллекций — легко представить именно Set'ом.
Итак, предлагаю ознакомиться с методами SortedMap на основании статьи (пункт «SortedMap»): https://metanit.com/java/tutorial/5.9.php
Здесь все просто и, в целом, похоже на уже изученный SortedSet. Даже так же, как и в случае с SortedSet, в статье почему-то забыт метод comparator(), возвращающий компаратор, на основании которого сделана сортировка. В SortedSet — значений, здесь — ключей.
Далее, продолжая аналогию, разберем наследника SortedMap — NavigableMap.
Интерфейс NavigableMap
Как вы, полагаю, догадались, ничего принципиально нового вы не увидите. NavigableMap по составу методов напоминает NavigableSet, с поправкой на особенности параметризации, из-за чего ряд методов (аналогичных ceiling(), higher(), floor() и lower() у NavigableSet) дублируются для Entry и для ключа.
Пункт «NavigableMap»: https://metanit.com/java/tutorial/5.9.php
Класс AbstractMap.SimpleEntry и другие реализации Map.Entry
Прежде чем перейти к реализациям Map, предлагаю ознакомиться с основными реализациями Map.Entry. Благо, это не займет много времени.
В случае, если вам понадобится создать изменяемую Entry — рекомендую воспользоваться вложенным классом AbstractMap — SimpleEntry. В том же классе можно найти и immutable аналог — вложенный класс SimpleImmutableEntry.
Обе реализации имеют конструкторы, создающие Entry на основании пары «ключ-значение», а также на основании другого Entry.
Альтернативой AbstractMap.SimpleImmutableEntry может выступить класс KeyValueHolder — именно его использует Map.entry(). Из минусов — KeyValueHolder не имеет конструктора для создания объекта на базе другого Entry, а также не является сериализуемым (что это значит — разберемся в свое время). В остальном эти реализации идентичны.
Класс HashMap
Наиболее простая и, одновременно, наиболее популярная реализация Map. Данные не отсортированы, порядок добавления не сохраняется. В целом, как HashSet, только HashMap:)
Познакомиться с конструкторами и использованием можно в рамках статьи (пункт «Классы отображений. HashMap»): https://metanit.com/java/tutorial/5.8.php
Класс LinkedHashMap
Реализация Map, сохраняющая порядок добавления элементов. Соответственно, методы keySet(), values() и entrySet() будут возвращать коллекции, хранящие ключи/значения/Entry в соответствии с порядком добавления.
По аналогии с соответствующими реализациями Set, LinkedHashMap является наследником HashMap. Но, в отличии от аналогичного Set'а, LinkedHashMap определяет ряд вложенных классов для реализации рассмотренных выше методов, а также переопределяет ряд методов HashMap, что и позволяет гарантировать нужный порядок элементов.
К слову, порядок элементов в LinkedHashSet тоже гарантируется именно из-за использования LinkedHashMap внутри.
Класс TreeMap
Единственная публичная реализация NavigableMap в java.util. Строится на основе уже знакомого нам RB-tree.
Как и соответствующий сет, хранит элементы в отсортированном виде (на основании Comparable или Comparator для ключа).
С конструкторами и использованием предлагаю познакомиться в статье (пункт «TreeMap»): https://metanit.com/java/tutorial/5.9.php
Другие реализации
Мы рассмотрели выше основные непотокобезопасные реализации в java.util. Из-за возможностей, которые дают ассоциативные массивы, публичных реализаций, заточенных под узкую специфику использования, у Map больше, чем у Set. Но в рамках данного урока мы их затрагивать не будем, ограничимся мапами общего назначения. Зато озвучим основные потокобезопасные реализации:
- legacy: классической legacy-реализацией Map является Hashtable;
- java.util.concurrent: ConcurrentHashMap как потокобезопасный аналог HashMap. Реализацией NavigableMap и, соответственно, аналогом TreeMap является ConcurrentSkipListMap.
Итог
На данном этапе можно утверждать, что мы познакомились со всеми типами коллекций в Java.
Это еще далеко не конец знакомства с Collection Framework — впереди еще разбор устройства HashMap, знакомство с коллекциями из java.util.concurrent в контексте изучения многопоточности, обработка коллекций в рамках ФП и разбор методов коллекций, которые были опущены на текущем этапе.
Тем не менее, сейчас мы достигли определенной черты — текущих знаний хватит, чтобы написать полноценное однопоточное приложение. Да, оно будет выглядеть несовременно. Да, оно вряд ли будет реализовано качественно в контексте архитектуры. Да, это будет консольное десктоп-приложение. И да, оно сможет сохранять результат своей работы в лучшем случае в файл, а не в БД. Но это уже результат и серьезное достижение.
Поэтому искренне поздравляю всех, кто дошел до этого этапа. Надеюсь, мне удалось сделать ваш путь менее тернистым, чем он был у меня и многих других разработчиков. Дальше будет… по-разному. Где-то будет удобнее и проще, где-то будет сложнее, где-то будет взрываться мозг. Но именно на данном этапе можно утверждать, что вы познакомились с необходимым базисом, на котором строится дальнейшее развитие Java-разработчика.
Так держать!
С теорией на сегодня все!

Переходим к практике:
Задача 1:
Реализуйте программу, выводящую в консоль количество использований каждого слова во введенной пользователем строке.
За основу предлагаю взять реализацию из задачи 3 урока 30.
Задача 2:
Реализуйте задачу из урока 19, используя Map. Реализацию выберите исходя из особенностей исходной задачи.
Если что-то непонятно или не получается – welcome в комменты к посту или в лс:)
Канал: https://t.me/+relA0-qlUYAxZjI6
Мой тг: https://t.me/ironicMotherfucker
Дорогу осилит идущий!