Принцип организации Map
в Java похож на то, как организованы списки:
- В стандартную библиотеку входит интерфейс
Map
, который описывает основные методы для работы коллекции. - Там же находятся конкретные реализации этого интерфейса, которые отличаются производительностью и потребляемыми ресурсами. Среди них самая универсальная и распространенная реализация это
HashMap
.
Рассмотрим работу HashMap
на примере задачи описания телефонного кода стран. В этой задаче мы хотим создать справочник, в котором каждой стране сопоставлен ее код, например, +1 для штатов. Ключом в этом справочнике будет название страны, значением - код.
var codes = new HashMap<String, Integer>();
codes.put("usa", 2); // добавляет значение
codes.put("usa", 1); // заменяет значение так как такой ключ уже существовал
codes.put("france", 33);
codes.put("germany", 49);
// Порядок в выводе не соответствует порядку добавления
// Так как в HashMap нет заданного порядка у ключей
System.out.println(codes); // => {usa=1, germany=49, france=33}
// Проверяем наличие ключа
codes.containsKey("Usa"); // false
codes.containsKey("usa"); // true
// Проверяем наличие значения
codes.containsValue(1); // true
codes.containsValue(3); // false
codes.get("usa"); // 1
codes.get("spain"); // null
codes.getOrDefault("spain", 1); // 1
codes.size(); // 3
codes.isEmpty(); // false
В этом примере мы создали пустой HashMap
в котором ключ это строка, а значение - число. Синтаксис построен по аналогии со списками. Типы данных, которые хранятся внутри указываются между символами <
и >
через запятую. Первый тип относится к ключу, второй к данным. Какие методы мы использовали:
put()
добавляет или изменяет значение ключаcontainsKey()
- проверяет наличие ключа в коллекции с учетом регистраget()
- возвращает элемент по имени ключа. Если ключ не найден, то возвращаетсяnull
.getOrDefault()
- в отличие отget()
, в случае если ключ не найден, возвращает значение по умолчанию переданное вторым параметромsize()
возвращает количество ключей в коллекцииisEmpty()
проверяет, является ли коллекция пустой
Кроме добавления ключей их можно удалять. Метод remove()
удаляет ключ, если он существует:
var codes = new HashMap<String, Integer>();
codes.put("usa", 1);
codes.remove("usa");
Если у коллекции есть начальный набор элементов и он достаточно большой, то их можно указать сразу с помощью метода Map.of()
. Единственное нужно учитывать, что Map.of()
создает неизменяемую коллекцию, то есть его элементы можно использовать, но изменять саму коллекцию нельзя. Чтобы добавить возможность ее изменения, нужно выполнить ее преобразование в HashMap
.
import java.util.HashMap;
import java.util.Map;
var map = Map.of("usa", 1, "england", 44);
var codes = new HashMap<String, Integer>(map);
codes.put("france", 33);
codes.put("germany", 49);
System.out.println(codes); // {england=44, france=33, usa=1, germany=49}
Как работают ключи
HashMap
внутри себя реализован с помощью хеш-таблиц. Хеш-таблица это структура данных, которая позволяет хранить, обновлять и удалять пары ключ-значение в обычных массивах. Зачем это нужно? Дело в том, что не существует прямого способа хранить Map
в памяти, для этого нужно придумывать какую-то систему упаковки данных в какую-то существующую структуру, например массив. Большая часть механики работы HashMap
спрятана от программиста. Но есть один элемент, с которым придется столкнуться явно. Это Хеш-код (Hash Code).
Хеш-код это число, которое формируется для каждого ключа добавленного в HashMap
. Оно используется для того, чтобы определить место в хеш-таблице для данной пары ключ-значение. Хеш-код формируется самим ключом. HashMap
лишь вызывает метод hashCode()
, который добавлен во все объекты Java. Пример со строками:
var name = "hexlet";
name.hashCode(); // -1220578016
Точно так же, метод hashCode()
присутствует и у всех объектов, определенных пользователем. Однако его реализация по умолчанию не подходит для работы с HashMap
. Она спроектирована так, чтобы возвращать различные числа для разных объектов с точки зрения адресации в памяти. При такой схеме каждый новый объект будет отличаться от других объектов. На практике же, даже разные объекты могут быть одинаковыми с точки зрения логики работы программы. Ярчайший пример этому строки. Две одинаковые строки как два разных объекта не равны друг другу, но у них одинаковый хеш-код и они равны друг другу, если использовать метод equals()
:
var name = new String("hexlet");
var name2 = new String("hexlet");
name == name2; // false
name.hashCode(); // -1220578016
name2.hashCode(); // -1220578016
name.equals(name2); // true
Если бы не такая реализация hashCode()
в строках, то код ниже бы не работал, как мы ожидаем:
var codes = new HashMap<String, Integer>();
// Тут используются два объекта строки. Один в put(), другой в get()
codes.put("usa", 1);
codes.get("usa"); // 1
В случае самостоятельно создаваемых классов это означает то, что нужно определять свою реализацию hashCode()
. Подробнее об этом мы поговорим, когда начнем глубже изучать ООП.
Дополнительные материалы
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.