(精)HashMap的4类遍历方式与性能剖析

Java
259
0
0
2023-05-24
标签   Java基础

开发中我们常用到hashmap的遍历,随着jdk8的发布,又多了lambda和stream api的遍历方式,如何选择合适的遍历方式?我们可以仔细分析一下。

本文 先从 HashMap 的遍历讲起,然后对性能进行测试、分析原理、安全性,来分析各种遍历方式的优点与不足 ,本文主要内容如下图所示:

(精)HashMap的4类遍历方式与性能剖析

HashMap的遍历

Hashmap的遍历方式可以分为四大类

  1. 迭代器方式遍历
  2. foreach方式遍历
  3. Streams Api方式遍历
  4. lambda表达式遍历

但每种类型下又有多种的表达方式,因此具体的遍历方式又可以分为以下 7 种:

  1. 使用迭代器EntrySet 的方式进行遍历;
  2. 使用迭代器KeySet 的方式进行遍历;
  3. 使用 foreach EntrySet 的方式进行遍历;
  4. 使用 foreach KeySet 的方式进行遍历;
  5. 使用 Streams Api 单线程的方式进行遍历;
  6. 使用 Streams Api 多线程的方式(parallel)进行遍历。
  7. 使用 lambda表达式的方式进行遍历;

接下来我们来看每种遍历的代码具体实现

1、迭代器EntrySet

(精)HashMap的4类遍历方式与性能剖析

上面程序输出结果为:

2、迭代器KeySet

(精)HashMap的4类遍历方式与性能剖析

以上程序输出结果为:

1->java 2->hadoop 3->spring 4->elasticsearch 5->flink

3、foreach EntrySet


(精)HashMap的4类遍历方式与性能剖析

以上程序输出结果为:

1->java 2->hadoop 3->spring 4->elasticsearch 5->flink

4、foreach KeySet


(精)HashMap的4类遍历方式与性能剖析

以上程序输出结果为:

1->java 2->hadoop 3->spring 4->elasticsearch 5->flink

5、Streams Api 单线程


(精)HashMap的4类遍历方式与性能剖析

以上程序输出结果为:

1->java 2->hadoop 3->spring 4->elasticsearch 5->flink

6、Streams Api 多线程


以上程序输出结果为:

4->elasticsearch 1->java 5->flink 2->hadoop 3->spring

7、lambda方式

以上程序输出结果为:

1->java 2->hadoop 3->spring 4->elasticsearch 5->flink

性能测试

下一步我们使用由Oracle官方提供的方法性能测试工具JMH(即Java Microbenchmark Harness),来测试一下这7个方法的性能。

 import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

@BenchmarkMode(Mode.Throughput) // 测试类型:吞吐量
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) // 预热 2 轮,每次 1s
@Measurement(iterations = 4, time = 3, timeUnit = TimeUnit.SECONDS) // 测试 4 轮,每次 3s
@Fork(1) // fork 1 个线程
@State(Scope.Thread) // 每个测试线程一个实例
public class HashmMapTest2 {
    static Map<Integer, String> map = new HashMap() {{
        // 添加数据
        for (int i = 0; i < 10; i++) {
            put(i, "v:" + i);
        }
    }};

public static void main(String[] args) throws RunnerException {
        // 启动基准测试
        Options opt = new OptionsBuilder()
                .include(HashmMapTest2.class.getSimpleName()) // 要导入的测试类
               .output("D:/jmh-maptest.log") // 输出测试结果的文件
               // .forks(1)
                .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();
            System.out.print(entry.getKey()+entry.getValue());
        }
    }

    @Benchmark
    public void keySet() {
        // 遍历
        Iterator<Integer> iterator = map.keySet().iterator();
        while (iterator.hasNext()) {
            Integer key = iterator.next();
            System.out.print(key+map.get(key));
        }
    }

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

    @Benchmark
    public void forEachKeySet() {
        // 遍历
        for (Integer key : map.keySet()) {
            System.out.print(key+map.get(key));
        }
    }

    @Benchmark
    public void lambda() {
        // 遍历
        map.forEach((key, value) -> {
            System.out.print(key+value);
        });
    }

    @Benchmark
    public void streamApi() {
        // 单线程遍历
        map.entrySet().stream().forEach((entry) -> {
            System.out.print(entry.getKey()+entry.getValue());
        });
    }

    @Benchmark
    public void parallelStreamApi() {
        // 多线程遍历
        map.entrySet().parallelStream().forEach((entry) -> {
            System.out.print(entry.getKey()+entry.getValue());
        });
    }
}  

所有被测试的方法(添加@Benchmark注解方法),测试结果如下:


(精)HashMap的4类遍历方式与性能剖析

其中Benchmark指的是调用的类的方法,Mode指的模式是吞吐量测试,Cnt指的测试多少轮,Score指的平均执行时间, ± Error是表示Score的误差。从上面结果可以得出结论, 在考虑上误差值的情况下,除了并行的parallelStream性能相比较高之外(结果乱序,多线程肯定比较快),其他方式的性能几乎没有明显差别。


性能原理分析

要理解性能测试的结果,我们可以通过反编译,查看代码的字节码查看原因,通过ideal查看字节码文件,内容如下。

 public class HashMapTest {
    public HashMapTest() {
    }
    @Test
    public void iteratorEntrySet() {
        Map<Integer, String> map = new HashMap();
        map.put(Integer.valueOf(1), "java");
        map.put(Integer.valueOf(2), "hadoop");
        map.put(Integer.valueOf(3), "spring");
        map.put(Integer.valueOf(4), "elasticsearch");
        map.put(Integer.valueOf(5), "flink");
        Set<Entry<Integer, String>> entries = map.entrySet();
        Iterator iterator = entries.iterator();

        while(iterator.hasNext()) {
            Entry<Integer, String> next = (Entry)iterator.next();
            System.out.print(next.getKey() + "->" + (String)next.getValue());
            System.out.print(" ");
        }

    }
   @Test
    public void iteratorKeySet() {
        Map<Integer, String> map = new HashMap();
        map.put(Integer.valueOf(1), "java");
        map.put(Integer.valueOf(2), "hadoop");
        map.put(Integer.valueOf(3), "spring");
        map.put(Integer.valueOf(4), "elasticsearch");
        map.put(Integer.valueOf(5), "flink");
        Set<Integer> integers = map.keySet();
        Iterator iterator = integers.iterator();

        while(iterator.hasNext()) {
            Integer key = (Integer)iterator.next();
            System.out.print(key + "->" + (String)map.get(key));
            System.out.print(" ");
        }

    }
   @Test
    public void forEachEntrySet() {
        Map<Integer, String> map = new HashMap();
        map.put(Integer.valueOf(1), "java");
        map.put(Integer.valueOf(2), "hadoop");
        map.put(Integer.valueOf(3), "spring");
        map.put(Integer.valueOf(4), "elasticsearch");
        map.put(Integer.valueOf(5), "flink");
        Iterator var2 = map.entrySet().iterator();

        while(var2.hasNext()) {
            Entry<Integer, String> entry = (Entry)var2.next();
            System.out.print(entry.getKey() + "->" + (String)entry.getValue() + " ");
        }

    }
    @Test
    public void forEachKeySet() {
        Map<Integer, String> map = new HashMap();
        map.put(Integer.valueOf(1), "java");
        map.put(Integer.valueOf(2), "hadoop");
        map.put(Integer.valueOf(3), "spring");
        map.put(Integer.valueOf(4), "elasticsearch");
        map.put(Integer.valueOf(5), "flink");
        Iterator var2 = map.keySet().iterator();

        while(var2.hasNext()) {
            Integer key = (Integer)var2.next();
            System.out.print(key + "->" + (String)map.get(key) + " ");
        }

    }
    @Test
    public void lambda() {
        Map<Integer, String> map = new HashMap();
        map.put(Integer.valueOf(1), "java");
        map.put(Integer.valueOf(2), "hadoop");
        map.put(Integer.valueOf(3), "spring");
        map.put(Integer.valueOf(4), "elasticsearch");
        map.put(Integer.valueOf(5), "flink");
        map.forEach((k, v) -> {
            System.out.print(k + "->" + v + " ");
        });
    }

 @Test
    public void singleStream() {
        Map<Integer, String> map = new HashMap();
        map.put(Integer.valueOf(1), "java");
        map.put(Integer.valueOf(2), "hadoop");
        map.put(Integer.valueOf(3), "spring");
        map.put(Integer.valueOf(4), "elasticsearch");
        map.put(Integer.valueOf(5), "flink");
        map.entrySet().stream().forEach((entry) -> {
            System.out.print(entry.getKey() + "->" + (String)entry.getValue() + " ");
        });
    }
    
     @Test
    public void parallelStream() {
        Map<Integer, String> map = new HashMap();
        map.put(Integer.valueOf(1), "java");
        map.put(Integer.valueOf(2), "hadoop");
        map.put(Integer.valueOf(3), "spring");
        map.put(Integer.valueOf(4), "elasticsearch");
        map.put(Integer.valueOf(5), "flink");
        map.entrySet().parallelStream().forEach((entry) -> {
            System.out.print(entry.getKey() + "->" + (String)entry.getValue() + " ");
        });
    }
}  

从结果可以看出,除了Streams 和Lambda 以外,通过迭代器和 for 循环的遍历EntrySet 最终生成的代码是一样的,如下所示:

(精)HashMap的4类遍历方式与性能剖析

而通过迭代器和 for 循环遍历的 KeySet 代码也是一样的,如下所示:

(精)HashMap的4类遍历方式与性能剖析

通过字节码可以看出,使用 EntrySet 和 KeySet 代码差别不是很大,并不像网上源码分析说的那样,KeySet 的get方法在获取Value 的时候需要进行遍历,性能上EntrySet 优于KeySet 。从测试结果看两种性能 几乎是相近的。

安全性测试

1、迭代器方式


(精)HashMap的4类遍历方式与性能剖析

以上程序输出结果为:

1->java删除:2
3->spring4->elasticsearch5->flink

测试总结:迭代器方式删除数据安全

2、for循环方式


(精)HashMap的4类遍历方式与性能剖析

以上程序输出结果为:


(精)HashMap的4类遍历方式与性能剖析

测试总结:for循环遍历方式删除数据不安全

3、lambda方式


(精)HashMap的4类遍历方式与性能剖析

以上程序输出结果为:


(精)HashMap的4类遍历方式与性能剖析

测试总结:lambda方式遍历删除数据不安全

4、stream 方式


(精)HashMap的4类遍历方式与性能剖析

以上程序运行结果为:


(精)HashMap的4类遍历方式与性能剖析

测试总结:streams方式遍历删除数据不安全

小结

我们不能在for循环遍历中使用时来删除map里面的数据,这是非安全的操作方式,但我们可以使用迭代器的 iterator.remove() 的方法来删除数据,这是安全的操作方式。同样的lambada和stream在使用的时候删除集合里面的数据是非安全的操作方式。但是我们可以在使用之前删除集合数据然后进行遍历。


总结

本文主要讲了hashmap的四大类遍历方式(迭代器,for循环、lambda、streams),以及具体的7种遍历方式代码, 除了stream的并行遍历外,其他几种遍历方式在性能上没有明显的差别,但是从代码的简洁和优雅上来看,lambda和stream有先天的优势。 除此之外,还从安全性的方面测试了四类遍历在运行时删除数据的安全性。 除了迭代方式外,其他三类在循环遍历的时候,删除数据是非安全的。但是我们可以先在遍历之前删除数据,然后进行遍历,这个操作方式是安全的。

总体来说,本文提供了 7 种常见的遍历方式,肯定不是最全的,我是想给读者在使用 HashMap 时选择最合适的方式, 然而选择哪一种写法,在性能、JDK 版本(是否1.8)、安全性以及优雅和可读性等方面来综合考虑 。最后,欢迎各位在评论区补充并留言,写出你们的想法。