zoukankan      html  css  js  c++  java
  • HashMap 的 7 种遍历方式与性能分析,高薪必学

    随着 JDK 1.8 Streams API 的发布,使得 HashMap 拥有了更多的遍历的方式,但应该选择那种遍历方式?反而成了一个问题。

    本文先从 HashMap 的遍历方法讲起,然后再从性能、原理以及安全性等方面,来分析 HashMap 各种遍历方式的优势与不足,本文主要内容如下图所示:

    image.png

    这里也要注意:不管你是为了JAVA高薪还是爱好,记住:项目开发经验永远是核心,如果你没有最新JAVA架构实战视频教程及大厂面试宝典,可以去小编的Java架构学习.裙 :七吧伞吧零而衣零伞 (数字的谐音)转换下可以找到了,里面很多新JAVA架构项目教程,还可以跟老司机交流讨教! 

    HashMap 遍历

    HashMap 遍历从大的方向来说,可分为以下 4 类:

    1. 迭代器(Iterator)方式遍历;
    2. For Each 方式遍历;
    3. Lambda 表达式遍历(JDK 1.8+);
    4. Streams API 遍历(JDK 1.8+)。

    但每种类型下又有不同的实现方式,因此具体的遍历方式又可以分为以下 7 种:

    1. 使用迭代器(Iterator)EntrySet 的方式进行遍历;
    2. 使用迭代器(Iterator)KeySet 的方式进行遍历;
    3. 使用 For Each EntrySet 的方式进行遍历;
    4. 使用 For Each KeySet 的方式进行遍历;
    5. 使用 Lambda 表达式的方式进行遍历;
    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.迭代器 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中文社群");
            // 遍历
            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 微基准测试套件)来测试一下这 7 种循环的性能。

    首先,我们先要引入 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 轮,每次 3s
    @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 为多线程版本性能一定是最好的,所以就不参与测试了,其他 6 个方法的测试结果如下:

    其中 Score 列表示平均执行时间, ± 符号表示误差。从以上结果可以看出,两个 entrySet 的性能相近,并且执行速度最快,接下来是 stream ,然后是两个 keySet,性能最差的是 lambda 表达式。

    注:以上结果基于测试环境:JDK 1.8 / Mac mini (2018) / Idea 2020.1

    小结

    因此我们应该尽量使用迭代器或 for 循环的 entrySet 遍历,或者是 stream 的方式。

    性能原理分析

    要理解性能测试的结果,我们需要把所有遍历代码通过 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));
        }
    }
    复制代码

    可以看出 KeySet 在循环中创建了一个 Integer 的局部变量,并且值是从 map 对象中直接获取的。

    所以通过上面测试结果可以看出,无论是通过迭代器方式还是 for 循环方式,最终的测试结果 EntrySet 的两个方法的性能都是相似的,而 KeySet  也是同理。

    安全性测试

    从上面的性能测试结果和原理分析,我想大家应该选用那种遍历方式,已经心中有数的,而接下来我们就从「安全」的角度入手,来分析那种遍历方式更安全。

    我们把以上遍历划分为四类进行测试:迭代器方式、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 循环方式

    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.Lambda 方式

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

    以上程序的执行结果:

    image.png测试结果:Lambda 循环中删除数据非安全。

    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.Stream 方式

    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 循环中删除数据非安全。

    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() 的方法来删除数据,这是安全的删除集合的方式。同样的我们也可以使用 Lambda 中的 removeIf 来提前删除数据,或者是使用 Stream 中的 filter 过滤掉要删除的数据进行循环,这样都是安全的,当然我们也可以在 for 循环前删除数据在遍历也是线程安全的。

    总结
    注意:不管你是为了JAVA高薪还是爱好,记住:项目开发经验永远是核心,如果你没有最新JAVA架构实战视频教程及大厂面试宝典,可以去小编的Java架构学习.裙 :七吧伞吧零而衣零伞 (数字的谐音)转换下可以找到了,里面很多新JAVA架构项目教程,还可以跟老司机交流讨教! 

    本文我们讲了 HashMap 4 种遍历方式:迭代器、for、lambda、stream,以及具体的 7 种遍历方法,综合性能和安全性来看,我们应该尽量使用迭代器 entrySet 的遍历方式,或者是 stream filter 的方式来遍历 HashMap。

    本文的文字及图片来源于网络加上自己的想法,仅供学习、交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理

  • 相关阅读:
    C语言 realloc为什么要有返回值,realloc返回值具体解释/(解决随意长度字符串输入问题)。
    opencv中的vs框架中的Blob Tracking Tests的中文注释。
    Java实现 蓝桥杯VIP 算法提高 棋盘多项式
    Java实现 蓝桥杯VIP 算法提高 棋盘多项式
    Java实现 蓝桥杯VIP 算法提高 棋盘多项式
    Java实现 蓝桥杯VIP 算法提高 棋盘多项式
    Java实现 蓝桥杯VIP 算法提高 分苹果
    Java实现 蓝桥杯VIP 算法提高 分苹果
    Java实现 蓝桥杯VIP 算法提高 分苹果
    Java实现 蓝桥杯VIP 算法提高 分苹果
  • 原文地址:https://www.cnblogs.com/chengxuyuanaa/p/12825904.html
Copyright © 2011-2022 走看看