Инициализация Java HashMap (коэффициент загрузки и начальная емкость)

Класс Java HashMap предоставляет хранилище пар ключ-значение с постоянной производительностью для операций get и put. Java позволяет инициализировать HashMap разными способами, и каждый из них служит определенной цели.

В этом руководстве по коллекциям Java рассматриваются различные методы инициализации HashMap, включая пустые карты, предварительно заполненные карты, неизменяемые карты и сбор потоковых элементов в карты.

 // 1. Пустые картыHashMap<Строка, Строка> map1 = new HashMap<>();HashMap<Строка, Строка> map2 = new HashMap<>(10);HashMap map3 = HashMap.newHashMap(10);// 2. Предварительно заполненные картыHashMap<Строка, Строка> map4 = new HashMap<>(Карта.записей(Карта.запись("ключ1", "значение1"),Карта.запись("ключ2", "значение3")));Карта<Строка, Строка> map5 = new HashMap<>() {{положить("ключ1", "значение1");положить("ключ2", "значение2");}};HashMap<Строка, Строка> map6 = new HashMap<>(Карта.of("ключ1", "значение1", "ключ2", "значение3"));// 3. Сбор потока для карты// Больше примеров: /collect-stream-to-map/Поток<Элемент> strem = ...;Карта mapWithValue = stream.collect(Collectors.toMap(Item::id, Item::name));

1. Краткий обзор грузоподъемности и коэффициента нагрузки

Если мы посмотрим на внутреннюю работу класса HashMap, то увидим два важных параметра: «initialCapacity» и «loadFactor».

  • Начальная емкость представляет собой количество контейнеров, которые может вместить HashMap при первом создании. Она определяет размер внутреннего массива, используемого для хранения пар ключ-значение. По умолчанию начальная емкость HashMap составляет 16.
  • Когда мы продолжаем добавлять пары ключ-значение, коэффициент загрузки определяет, насколько заполненной может быть HashTable, прежде чем ее нужно будет изменить. В классе HashMap коэффициент загрузки по умолчанию равен 0,75.

Когда количество элементов в HashMap превышает произведение коэффициента загрузки и текущей емкости, размер HashMap изменяется для размещения большего количества элементов. Во время изменения размера JVM создает новый внутренний массив с большей емкостью(вычисляемой) и сохраняет все существующие пары ключ-значение в новом массиве.

Эта операция изменения размера является затратной по времени и ресурсам. Вот почему рекомендуется указать приблизительное количество записей, которые будет хранить карта. Это позволяет JVM заранее выделить достаточно большой массив и, таким образом, избежать операции изменения размера.

2. Инициализируйте пустой HashMap

Пустая карта не содержит пар ключ-значение. Она инициализируется без каких-либо записей.

Лучший способ создать пустой HashMap — использовать его конструктор по умолчанию. Он инициализирует экземпляр HashMap с начальной емкостью 16 и коэффициентом загрузки 0,75.

 HashMap<Строка, Строка> карта = новая HashMap<>();

В качестве альтернативы мы можем указать начальную емкость в конструкторе. Это полезно, когда мы знаем приблизительное количество элементов, которые будет содержать карта. Это может улучшить производительность во время выполнения, уменьшив количество операций изменения размера.

 // Начальная емкость — 10, но изменение размера срабатывает при сохранении 8-го элемента.HashMap<Строка, Строка> карта = новая HashMap<>(10);

Похожий метод newHashMap() был введен в Java 19, который вычисляет приблизительную начальную емкость, используя метод input. Это «статический» метод, поэтому нам не нужно вызывать конструктор.

// Начальная емкость — 14, изменение размера срабатывает при сохранении 11-го элементаHashMap map = HashMap.newHashMap(10);

3. Разница между конструктором new HashMap() и методом newHashMap()

Оба метода выглядят одинаково, так в чем же разница?

Главное различие между конструктором 'new HashMap(capacity)' и методом 'newHashMap(capacity)' заключается в роли коэффициента загрузки. Предположим, вы хотите создать ArrayList из 180 элементов, вы можете использовать конструктор 'new HashMap(180)' и надеяться, что он сохранит 180 пар без изменения размера. Но этого не происходит. В действительности, из-за коэффициента загрузки по умолчанию 0,75, изменение размера запускается, как только он заполняется на 75%(180 × 0,75 = 135 пар). Ой!!

Чтобы исправить это поведение, новый метод фабрики newHashMap пересчитывает емкость, деля количество требуемых пар на коэффициент загрузки. Например, когда мы используем 'newHashMap(180)', он создает Map с начальной емкостью 240(180 ÷ 0,75 = 240). Таким образом, изменение размера не сработает, пока мы не сохраним 180 элементов. [ Читать далее ]

После инициализации HashMap мы можем добавлять и удалять записи из карты.

map.put("key1", "value1");map.put("key2", "value2");String value1 = map.get("key1");String value2 = map.get("key2");

4. Инициализируйте HashMap с помощью пар «ключ-значение»

Несколько раз у нас уже есть пары ключ-значение, которые должны быть сохранены в HashMap. В этом случае имеет смысл использовать соответствующий конструктор для инициализации и заполнения пар в одном операторе.

Следующий конструктор берет существующую карту и копирует все ее записи в новый объект HashMap.

 HashMap<Строка, Строка> карта = новая HashMap<>(Карта.записей(Карта.запись("ключ1", "значение1"),Карта.запись("ключ2", "значение3")));

В качестве альтернативы мы можем использовать аналогичный подход с использованием методов Map.of(), которые позволяют нам создать неизменяемую карту с максимум 10 парами ключ-значение.

 HashMap<Строка, Строка> карта = новая HashMap<>(Карта.of("ключ1", "значение1", "ключ2", "значение3"));

Похожий синтаксис использует инициализацию с двойными скобками, что не рекомендуется. При таком подходе создается анонимный подкласс HashMap, который содержит ссылку на включающий экземпляр внешнего класса. Этот дополнительный экземпляр может привести к утечке памяти, если объект внешней карты не будет собран сборщиком мусора.

 Карта<Строка, Строка> карта = new HashMap<>() {{положить("ключ1", "значение1");положить("ключ2", "значение2");}}

5. Сбор элементов потока в HashMap

Иногда у нас могут быть пары ключ-значение заранее. Вместо этого мы получим пары из динамического источника, такого как Stream. API Stream предоставляет удобный API для обработки и хранения всех элементов потока в Map.

 Поток<Элемент> поток = Поток.из(новый Элемент(1, "Элемент1"),новый Элемент(2, "Элемент2"),новый Элемент(3, "Элемент3"));Карта<Длинная, Строка> mapWithValue = поток.collect(Collectors.toMap(Item::id, Item::name));

Вышеуказанный экземпляр Map неизменяем. Мы не можем добавить к нему больше пар ключ-значение. Если нам нужен экземпляр HashMap, чтобы мы могли изменить его позже, мы можем сделать следующее:

 Карта<Длинная, Строка> mapWithValue = поток.collect(Collectors.toMap(Item::id, Item::name,(old, new) -> old, HashMap::new));

Очень важно знать заранее, будут ли элементы Stream иметь разное значение для поля ключа карты или нет. Если ключи карты дублируются и мы используем метод Collectors.toMap(), мы получим IllegalStateException.

Если в потоке присутствуют дублирующиеся ключи, то мы можем использовать Collectors.groupingBy() для сбора элементов потока.

Stream<Item> streamWithDuplicates = Stream.of(new Item(1, "Item1"),new Item(2, "Item2"),new Item(3, "Item3-1"),new Item(3, "Item3-2"),new Item(3, "Item3-3"));Map<Long, List<Item>> mapWithList = streamWithDuplicates.collect(Collectors.groupingBy(Item::id));

6. Резюме

В этом уроке Java мы научились инициализировать объект HashMap разными способами, такими как пустая карта, предварительно полированная карта и даже сбор элементов потока в карту. Мы изучили эти методы на примерах и поняли, когда мы можем использовать эти методы на основе требований.

Исходный код на Github

Прокрутить вверх