Итерация по HashMap: сравнение производительности

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

Рекомендуется изучить, как HashMap работает внутри, чтобы знать, как данные хранятся в HashMap. Если мой последний похожий пост, мы сравнивали различные «циклы for», доступные в Java. Эти исследования обычно помогают в настройке лучших практик для вашего следующего проекта.

1. Введение

В этом посте я решил сравнить производительность различных способов обхода HashMap в Java. HashMap — очень широко используемый класс, и большую часть времени мы извлекаем значение с помощью метода get(Object key), предоставляемого классом. Но иногда требуется перебрать всю Map и извлечь все пары ключ-значение, хранящиеся в ней.

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

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

2. Различные способы итерации по карте

Давайте начнем с различных способов итерации по HashMap. HashMap был определен следующим образом:

Map<String, Integer> map = new HashMap();

2.1.HashMap.forEach()

Начиная с Java 8, мы можем использовать метод forEach() для перебора ключей и значений, хранящихся в Map.

map.forEach((key, value) -> {System.out.println(key + ": " + value);//...});

2.2. Итерация по записям карты

Метод entrySet() возвращает все записи в представлении Set, которые мы можем перебирать аналогично обычному HashSet в Java.

for(Map.Entry<String, Integer> entry : map.entrySet()) {String key = entry.getKey();Integer value = entry.getValue();//...}

2.3. Итерация по ключам карты и доступ к значениям

Метод keySet() возвращает все ключи как Set. Мы можем перебрать все ключи и использовать их для доступа к значениям из Map. Обратите внимание, что для доступа к значению требуется дополнительный шаг, поэтому в большинстве случаев это не рекомендуемый подход.

for(String key : map.keySet()) {Integer value = map.get(key);//...}

2.4 Использование итератора

Поскольку мы получаем представление Set из entrySet() и keySet(), мы можем использовать Iterator для итерации по Map.

Следующий код выполняет итерацию по записям карты с помощью итератора.

Iterator<Entry<String, Integer>> entryIterator = map.entrySet().iterator();while(entryIterator.hasNext()) {Map.Entry<String, Integer> entry = entryIterator.next();String key = entry.getKey();Integer value = entry.getValue();//...}

Аналогично, следующий код выполняет итерацию по ключам карты с помощью итератора и получает доступ к значениям с помощью ключей.

Iterator<String> keySetIterator = map.keySet().iterator();while(keySetIterator.hasNext()) {String key = keySetIterator.next();Integer value = map.get(key);//...}

3. Сравнение эффективности различных методов

Теперь давайте сравним производительность всех типов итераций для общего набора данных, хранящегося в Map. Мы храним 1 миллион пар ключ-значение в Map.

Карта карта = new HashMap();для(int i = 0; i < 10_00_000; i++) {map.put(String.valueOf(i), i);}

Мы выполним итерацию по карте всеми четырьмя способами. Мы также извлечем ключ и значение из карты для всех записей наиболее подходящим способом. Мы используем модуль JMH для сравнительного анализа производительности всех методов.

@Benchmarkpublic void usingForEach(Blackhole blackhole) {map.forEach((key, value) -> {blackhole.consume(key);blackhole.consume(value);});}@Benchmarkpublic void usingEntrySetWithForLoop(Blackhole blackhole) {for(Map.Entry<String, Integer> entry : map.entrySet()) {blackhole.consume(entry.getKey());blackhole.consume(entry.getValue());}}@Benchmarkpublic void usingKeySetWithForLoop(Blackhole blackhole) {for(String key : map.keySet()) {blackhole.consume(map.get(key));}}@Benchmarkpublic void usingEntrySetWithForIterator(Blackhole blackhole) {Iterator<Entry<String, Integer>> entryIterator = map.entrySet().iterator();while(entryIterator.hasNext()) {Map.Entry<String, Integer> entry = entryIterator.next();blackhole.consume(entry.getKey());blackhole.consume(entry.getValue());}}

4. Результат

Сравнительный балл вышеуказанной программы:

c.h.c.collections.map...usingForEach thrpt 15 89.044 ± 2.703 ops/sc.h.c.collections.map...usingEntrySetWithForLoop thrpt 15 54.906 ± 6.326 ops/sc.h.c.collections.map...usingKeySetWithForLoop thrpt 15 52.163 ± 4.517 ops/sc.h.c.collections.map...usingEntrySetWithForIterator thrpt 15 63.494 ± 4.334 ops/s

Хотя между всеми методами mobe нет большой разницы, мы можем видеть, что:

  • При использовании цикла for с методами keySet() и entrySet() производительность практически одинакова.
  • Использование методов iterator() работает сравнительно хуже.
  • Удивительно, но метод forEach() занимает больше всего времени. Это потому, что метод forEach использует внутреннюю итерацию, что означает, что логика итерации обрабатывается внутри реализации HashMap. Он вызывает предоставленное лямбда-выражение для каждого элемента в HashMap. Эта внутренняя итерация сопровождается некоторыми накладными расходами из-за выполнения лямбда-выражения и дополнительных вызовов функций.

Ради чистоты кода можно заключить, что:

  • Если Map содержит только несколько записей, то использование HashMap.forEach() — лучший способ перебрать HashMap.
  • Если Map содержит только миллион записей, то использование HashMap.entrySet() — лучший способ перебрать HashMap.

5. Заключение

10 лаков — это очень большое число для большинства требований приложения. Хотя разница не очень существенна в миллисекундах, по сравнению с тем, что было очень большим в случае циклов for. Я считаю, что большинство из нас могут жить с такой незначительной разницей.

Но если вы хотите сделать вывод, использование набора записей очень конкретно является более мощным и дает лучшую производительность, чем другие. Результат варьируется от 20% до 50%, когда указанная выше программа выполняется несколько раз.

Пожалуйста, дайте мне знать, что вы думаете по поводу вышеприведенного анализа.

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

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