7 методов обхода и анализ производительности HashMap!

Java
7 методов обхода и анализ производительности HashMap!

С выпуском JDK 1.8 Streams API у HashMap появилось больше методов обхода, но какой метод обхода выбрать? Вместо этого это стало проблемой.

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

image.png

Обход HashMap

HashMap Траверсы можно разделить на следующие 4 категории по общему направлению:

  1. Итератор (Iterator) способ обхода;
  2. Для каждого пути прохождения;
  3. обход лямбда-выражений (JDK 1.8+);
  4. Обход API потоков (JDK 1.8+).

Однако каждый тип имеет разные методы реализации, поэтому конкретные методы обхода можно разделить на следующие 7 типов:

  1. Обход с помощью итератора (Iterator) EntrySet;
  2. Обход с помощью итератора (Iterator) KeySet;
  3. Используйте For Each EntrySet для обхода;
  4. Используйте For Each KeySet для обхода;
  5. Обход с помощью лямбда-выражений;
  6. Используйте Streams API для однопоточного обхода;
  7. Обход выполняется в многопоточном режиме с использованием Streams API.

Далее давайте посмотрим на конкретный код реализации каждого метода обхода.

1. Итератор EntrySet

public class HashMapTest {
    public static void main(String[] args) {
        // 创建并赋值 HashMap
        Map<Integer, String> map = new HashMap();
        map.put(1, "Java");
        map.put(2, "JDK");
        map.put(3, "Spring Framework");
        map.put(4, "MyBatis framework");
        map.put(5, "Java中文社群");
        // 遍历
        Iterator<Map.Entry<Integer, String>> iterator = map.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<Integer, String> entry = iterator.next();
            System.out.println(entry.getKey());
            System.out.println(entry.getValue());
        }
    }
}

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

1

Java

2

JDK

3

Spring Framework

4

MyBatis framework

5

Сообщество китайского языка Java

2. Набор ключей итератора

public class HashMapTest {
    public static void main(String[] args) {
        // 创建并赋值 HashMap
        Map<Integer, String> map = new HashMap();
        map.put(1, "Java");
        map.put(2, "JDK");
        map.put(3, "Spring Framework");
        map.put(4, "MyBatis framework");
        map.put(5, "Java中文社群");
        // 遍历
        Iterator<Integer> iterator = map.keySet().iterator();
        while (iterator.hasNext()) {
            Integer key = iterator.next();
            System.out.println(key);
            System.out.println(map.get(key));
        }
    }
}

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

1

Java

2

JDK

3

Spring Framework

4

MyBatis framework

5

Сообщество китайского языка Java

3.ForEach EntrySet

public class HashMapTest {
    public static void main(String[] args) {
        // 创建并赋值 HashMap
        Map<Integer, String> map = new HashMap();
        map.put(1, "Java");
        map.put(2, "JDK");
        map.put(3, "Spring Framework");
        map.put(4, "MyBatis framework");
        map.put(5, "Java中文社群");
        // 遍历
        for (Map.Entry<Integer, String> entry : map.entrySet()) {
            System.out.println(entry.getKey());
            System.out.println(entry.getValue());
        }
    }
}

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

1

Java

2

JDK

3

Spring Framework

4

MyBatis framework

5

Сообщество китайского языка Java

4.ForEach KeySet

public class HashMapTest {
    public static void main(String[] args) {
        // 创建并赋值 HashMap
        Map<Integer, String> map = new HashMap();
        map.put(1, "Java");
        map.put(2, "JDK");
        map.put(3, "Spring Framework");
        map.put(4, "MyBatis framework");
        map.put(5, "Java中文社群");
        // 遍历
        for (Integer key : map.keySet()) {
            System.out.println(key);
            System.out.println(map.get(key));
        }
    }
}

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

1

Java

2

JDK

3

Spring Framework

4

MyBatis framework

5

Сообщество китайского языка Java

5.Lambda

public class HashMapTest {
    public static void main(String[] args) {
        // 创建并赋值 HashMap
        Map<Integer, String> map = new HashMap();
        map.put(1, "Java");
        map.put(2, "JDK");
        map.put(3, "Spring Framework");
        map.put(4, "MyBatis framework");
        map.put(5, "Java中文社群");
        // 遍历
        map.forEach((key, value) -> {
            System.out.println(key);
            System.out.println(value);
        });
    }
}

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

1

Java

2

JDK

3

Spring Framework

4

MyBatis framework

5

Сообщество китайского языка Java

6. Однопоточность Streams API

public class HashMapTest {
    public static void main(String[] args) {
        // 创建并赋值 HashMap
        Map<Integer, String> map = new HashMap();
        map.put(1, "Java");
        map.put(2, "JDK");
        map.put(3, "Spring Framework");
        map.put(4, "MyBatis framework");
        map.put(5, "Java中文社群");
        // 遍历
        map.entrySet().stream().forEach((entry) -> {
            System.out.println(entry.getKey());
            System.out.println(entry.getValue());
        });
    }
}

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

1

Java

2

JDK

3

Spring Framework

4

MyBatis framework

5

Сообщество китайского языка Java

7. Многопоточность Streams API

public class HashMapTest {
    public static void main(String[] args) {
        // 创建并赋值 HashMap
        Map<Integer, String> map = new HashMap();
        map.put(1, "Java");
        map.put(2, "JDK");
        map.put(3, "Spring Framework");
        map.put(4, "MyBatis framework");
        map.put(5, "Java中文社群");
        // 遍历
        map.entrySet().parallelStream().forEach((entry) -> {
            System.out.println(entry.getKey());
            System.out.println(entry.getValue());
        });
    }
}

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

4

MyBatis framework

5

Сообщество китайского языка Java

1

Java

2

JDK

3

Spring Framework

Тестирование производительности

Затем мы используем официальный инструмент тестирования производительности Oracle JMH (Java Microbenchmark Harness, JAVA Microbenchmark Test Suite) для проверки производительности этих семи циклов.

Прежде всего, нам нужно представить структуру JMH, вpom.xmlДобавьте в файл следующую конфигурацию:

<!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core -->
<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-core</artifactId>
    <version>1.23</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-generator-annprocess -->
<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-generator-annprocess</artifactId>
    <version>1.23</version>
    <scope>provided</scope>
</dependency>

Затем напишите тестовый код следующим образом:

@BenchmarkMode(Mode.AverageTime) // 测试完成时间
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) // 预热 2 轮,每次 1s
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) // 测试 5 轮,每次 1s
@Fork(1) // fork 1 个线程
@State(Scope.Thread) // 每个测试线程一个实例
public class HashMapCycleTest {
    static Map<Integer, String> map = new HashMap() {{
        // 添加数据
        for (int i = 0; i < 100; i++) {
            put(i, "val:" + i);
        }
    }};

    public static void main(String[] args) throws RunnerException {
        // 启动基准测试
        Options opt = new OptionsBuilder()
                .include(HashMapCycle.class.getSimpleName()) // 要导入的测试类
                .output("/Users/admin/Desktop/jmh-map.log") // 输出测试结果的文件
                .build();
        new Runner(opt).run(); // 执行测试
    }

    @Benchmark
    public void entrySet() {
        // 遍历
        Iterator<Map.Entry<Integer, String>> iterator = map.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<Integer, String> entry = iterator.next();
            Integer k = entry.getKey();
            String v = entry.getValue();
        }
    }

    @Benchmark
    public void forEachEntrySet() {
        // 遍历
        for (Map.Entry<Integer, String> entry : map.entrySet()) {
            Integer k = entry.getKey();
            String v = entry.getValue();
        }
    }

    @Benchmark
    public void keySet() {
        // 遍历
        Iterator<Integer> iterator = map.keySet().iterator();
        while (iterator.hasNext()) {
            Integer k = iterator.next();
            String v = map.get(k);
        }
    }

    @Benchmark
    public void forEachKeySet() {
        // 遍历
        for (Integer key : map.keySet()) {
            Integer k = key;
            String v = map.get(k);
        }
    }

    @Benchmark
    public void lambda() {
        // 遍历
        map.forEach((key, value) -> {
            Integer k = key;
            String v = map.get(k);
        });
    }

    @Benchmark
    public void streamApi() {
        // 单线程遍历
        map.entrySet().stream().forEach((entry) -> {
            Integer k = entry.getKey();
            String v = entry.getValue();
        });
    }

    public void parallelStreamApi() {
        // 多线程遍历
        map.entrySet().parallelStream().forEach((entry) -> {
            Integer k = entry.getKey();
            String v = entry.getValue();
        });
    }
}

все добавлено@BenchmarkАннотированные методы будут тестироваться, так как производительность parallelStream должна быть лучшей для многопоточной версии, поэтому он не будет участвовать в тесте.Результаты тестирования остальных шести методов следующие:

где столбец Score представляет среднее время выполнения,±Символ указывает на ошибку. Из вышеприведенных результатов видно, чтоlambdaвыражение и дваentrySetаналогичен по производительности и работает быстрее всего, за ним следуетstream, то дваkeySet.

Примечание. Приведенные выше результаты основаны на тестовой среде: JDK 1.8 / Mac mini (2018 г.) / Idea 2020.1.

в заключении

С точки зрения производительности мы должны попытаться использоватьlambdaилиentrySetпересекатьMapсобирать.

Анализ байт-кода

Чтобы понять приведенные выше результаты теста, нам нужно передать весь код обхода черезjavacСкомпилируйте в байт-код, чтобы увидеть конкретную причину.

После компиляции мы используем Idea для открытия байт-кода со следующим содержимым:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.example;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;

public class HashMapTest {
    static Map<Integer, String> map = new HashMap() {
        {
            for(int var1 = 0; var1 < 2; ++var1) {
                this.put(var1, "val:" + var1);
            }

        }
    };

    public HashMapTest() {
    }

    public static void main(String[] var0) {
        entrySet();
        keySet();
        forEachEntrySet();
        forEachKeySet();
        lambda();
        streamApi();
        parallelStreamApi();
    }

    public static void entrySet() {
        Iterator var0 = map.entrySet().iterator();

        while(var0.hasNext()) {
            Entry var1 = (Entry)var0.next();
            System.out.println(var1.getKey());
            System.out.println((String)var1.getValue());
        }

    }

    public static void keySet() {
        Iterator var0 = map.keySet().iterator();

        while(var0.hasNext()) {
            Integer var1 = (Integer)var0.next();
            System.out.println(var1);
            System.out.println((String)map.get(var1));
        }

    }

    public static void forEachEntrySet() {
        Iterator var0 = map.entrySet().iterator();

        while(var0.hasNext()) {
            Entry var1 = (Entry)var0.next();
            System.out.println(var1.getKey());
            System.out.println((String)var1.getValue());
        }

    }

    public static void forEachKeySet() {
        Iterator var0 = map.keySet().iterator();

        while(var0.hasNext()) {
            Integer var1 = (Integer)var0.next();
            System.out.println(var1);
            System.out.println((String)map.get(var1));
        }

    }

    public static void lambda() {
        map.forEach((var0, var1) -> {
            System.out.println(var0);
            System.out.println(var1);
        });
    }

    public static void streamApi() {
        map.entrySet().stream().forEach((var0) -> {
            System.out.println(var0.getKey());
            System.out.println((String)var0.getValue());
        });
    }

    public static void parallelStreamApi() {
        map.entrySet().parallelStream().forEach((var0) -> {
            System.out.println(var0.getKey());
            System.out.println((String)var0.getValue());
        });
    }
}

Как видно из результатов, помимо Lambda и Streams API, цикл по итератору иforциклический обходEntrySetОкончательный сгенерированный код одинаков, все они создают объект обхода в цикле.Entry,Следующее:

public static void entrySet() {
    Iterator var0 = map.entrySet().iterator();
    while(var0.hasNext()) {
        Entry var1 = (Entry)var0.next();
        System.out.println(var1.getKey());
        System.out.println((String)var1.getValue());
    }
}
public static void forEachEntrySet() {
    Iterator var0 = map.entrySet().iterator();
    while(var0.hasNext()) {
        Entry var1 = (Entry)var0.next();
        System.out.println(var1.getKey());
        System.out.println((String)var1.getValue());
    }
}

при передаче итераторов иforциклически проходимыйKeySetКод также тот же, а именно:

public static void keySet() {
    Iterator var0 = map.keySet().iterator();
    while(var0.hasNext()) {
        Integer var1 = (Integer)var0.next();
        System.out.println(var1);
        System.out.println((String)map.get(var1));
    }
} 
public static void forEachKeySet() {
    Iterator var0 = map.keySet().iterator();
    while(var0.hasNext()) {
        Integer var1 = (Integer)var0.next();
        System.out.println(var1);
        System.out.println((String)map.get(var1));
    }
}

Итак, мы используем итераторы илиforциклEntrySet, их производительность одинакова, потому что байт-коды, которые они в конечном итоге генерируют, в основном одинаковы;KeySetОба метода обхода также схожи.

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

EntrySetпочему, чемKeySetВысокая производительность обусловлена ​​тем, чтоKeySetиспользуется во время циклаmap.get(key)map.get(key)Это эквивалентно повторному обходуMapколлекция для запросаkeyсоответствующее значение. Зачем использовать слово "опять"? Это потому, что при использовании итераторов илиforПри зацикливании он фактически был пройден один разMapколлекция, так что используйте сноваmap.get(key)При запросе это эквивалентно обходу дважды.

иEntrySetпройдено всего один разMapколлекции, а затем передайте код "Entry entry = iterator.next()" объектуkeyиvalueзначения вставляютсяEntryобъект, так что получитьkeyиvalueВам не нужно просматривать коллекцию Map, когда вы получаете значение, вам просто нужно получить значение из объекта Entry.

так,EntrySetкоэффициент производительностиKeySetпроизводительность удваивается, потому чтоKeySetЭквивалентно двойному циклуMapколлекция, иEntrySetтолько цикл один раз.

тестирование безопасности

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

Мы разделяем вышеуказанный обход на четыре категории для тестирования: метод итератора, метод цикла For, метод Lambda и метод Stream, Код теста выглядит следующим образом.

1. Метод итератора

Iterator<Map.Entry<Integer, String>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
    Map.Entry<Integer, String> entry = iterator.next();
    if (entry.getKey() == 1) {
        // 删除
        System.out.println("del:" + entry.getKey());
        iterator.remove();
    } else {
        System.out.println("show:" + entry.getKey());
    }
}

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

show:0

del:1

show:2

Результаты теста:Цикл итератора удаляет безопасность данных.

2. Метод цикла

for (Map.Entry<Integer, String> entry : map.entrySet()) {
    if (entry.getKey() == 1) {
        // 删除
        System.out.println("del:" + entry.getKey());
        map.remove(entry.getKey());
    } else {
        System.out.println("show:" + entry.getKey());
    }
}

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

image.png

Результаты теста:Удаление данных в цикле For небезопасно.

3. Лямбда-метод

map.forEach((key, value) -> {
    if (key == 1) {
        System.out.println("del:" + key);
        map.remove(key);
    } else {
        System.out.println("show:" + key);
    }
});

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

image.png
Результаты теста:Удаление данных в лямбда-цикле небезопасно.

Правильный способ удаления Lambda:

// 根据 map 中的 key 去判断删除
map.keySet().removeIf(key -> key == 1);
map.forEach((key, value) -> {
    System.out.println("show:" + key);
});

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

show:0

show:2

Как видно из приведенного выше кода, вы можете сначала использоватьLambda изremoveIfУдаление лишних данных, а затем зацикливание — правильный способ манипулирования коллекциями.

4. Поточный метод

map.entrySet().stream().forEach((entry) -> {
    if (entry.getKey() == 1) {
        System.out.println("del:" + entry.getKey());
        map.remove(entry.getKey());
    } else {
        System.out.println("show:" + entry.getKey());
    }
});

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

image.png

Результаты теста:Небезопасно удалять данные в цикле Stream.

Правильный способ зацикливания потока:

map.entrySet().stream().filter(m -> 1 != m.getKey()).forEach((entry) -> {
    if (entry.getKey() == 1) {
        System.out.println("del:" + entry.getKey());
    } else {
        System.out.println("show:" + entry.getKey());
    }
});

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

show:0

show:2

Как видно из приведенного выше кода, вы можете использоватьStream серединаfilterФильтрация бесполезных данных и последующий обход также являются безопасным способом манипулирования коллекциями.

в заключении

Мы не можем использовать коллекции в обходеmap.remove()Чтобы удалить данные, это небезопасный режим работы, но мы можем использовать итераторiterator.remove()Способ удаления данных, который является безопасным способом удаления коллекции. Точно так же мы также можем использовать лямбда вremoveIfудалить данные заранее или использоватьfilterБезопасно отфильтровывать удаляемые данные для зацикливания.Конечно, мы также можемforУдаление данных перед циклом также является потокобезопасным во время обхода.

Суммировать

В этой статье мы рассказали о 4 методах обхода HashMap: iterator, for, lambda, stream и 7 конкретных методах обхода, С точки зрения комплексной производительности и безопасности,мы должны попытаться использоватьentrySetдля обхода HashMap, что является безопасным и эффективным.

последние слова

Оригинальность – это непросто, если вы считаете, что эта статья вам полезна, нажмите на значок "отличный", это самая большая поддержка и поощрение для автора, спасибо.

Ссылки и благодарности

Woohoo. Java guides.net/2020/03/5 - нет...

Подпишитесь на официальный аккаунт «Java Chinese Community» и ответьте на «Галантные товары», чтобы получить 50 оригинальных галантерейных товаров.Топ-лист.