面试官问我HashMap的遍历方式,我懵了….

面试开始

今天在面试过程中,面试官问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 多线程的方式进行遍历。

下来之后我就抓紧把这七种方式都手敲了一遍,复习熟悉了一下。

HashMap的遍历方式

首先准备一个Hashmap集合类,并添加一些数据:

HashMap<String, Integer> map = new HashMap<>();
map.put("mysql", 80);
map.put("java", 90);
map.put("redis", 80);
map.put("spring", 60);

 

 

迭代器方式遍历

1、迭代器遍历EntrySet

// 基于 Iterator 迭代器 的 EntrySet 方式
Iterator<Map.Entry<String, Integer>> iterator1 = map.entrySet().iterator();
while (iterator1.hasNext()){
    Map.Entry<String, Integer> entry = iterator1.next();
    System.out.println("key=" + entry.getKey() + "----------value=" + entry.getValue());
}

// 打印输出:
// key=spring----------value=60
// key=java----------value=90
// key=mysql----------value=80
// key=redis----------value=80

 

 

2、迭代器遍历KeySet

 Iterator<String> iterator = map.keySet().iterator();
 while (iterator.hasNext()){
     String key = iterator.next();
     System.out.println("key=" + key + "----------value=" + map.get(key));
 }
 ​
 // 打印输出:
 // key=spring----------value=60
 // key=java----------value=90
 // key=mysql----------value=80
 // key=redis----------value=80

 

 

ForEach方式遍历

3、ForEach遍历EntrySet

for (Map.Entry<String, Integer> entry: map.entrySet()) {
    System.out.println("key=" + entry.getKey() + "----------value=" + entry.getValue());
}

// 打印输出:
// key=spring----------value=60
// key=java----------value=90
// key=mysql----------value=80
// key=redis----------value=80

 

 

4、ForEach遍历KeySet

 for (String key: map.keySet()) {
     System.out.println("key=" + key + "----------value=" + map.get(key));
 }
 ​
 // 打印输出:
 // key=spring----------value=60
 // key=java----------value=90
 // key=mysql----------value=80
 // key=redis----------value=80

 

 

Lambda表达式

5、Lambda表达式

 map.forEach((k, v) -> {
     System.out.println("key=" + k + "----------value=" + v);
 });
 ​
 // 打印输出:
 // key=spring----------value=60
 // key=java----------value=90
 // key=mysql----------value=80
 // key=redis----------value=80

 

 

Stream API 方式遍历

6、Stream API stream()遍历

 map.entrySet().stream().forEach((entry) -> {
     System.out.println("key=" + entry.getKey() + "----------value=" + entry.getValue());
 });
 ​
 // 打印输出:
 // key=spring----------value=60
 // key=java----------value=90
 // key=mysql----------value=80
 // key=redis----------value=80

 

 

7、Stream API parallelStream()遍历

 map.entrySet().parallelStream().forEach((entry) -> {
     System.out.println("key=" + entry.getKey() + "----------value=" + entry.getValue());
 });
 ​
 // 打印输出:
 // key=spring----------value=60
 // key=java----------value=90
 // key=mysql----------value=80
 // key=redis----------value=80

 

 

HashMap批量删除元素

紧接着,面试官又问:如何在HashMap中批量删除元素?

我当时有点懵,批量删除??我不记得HashMap有批量删除的API啊??我还傻乎乎的说是不是有一个方法呢?我记不太清了,结果说是没有,然后我就说没有批量删除操作过........接着面试官就说假如现在就有这么一个需求你会怎么做,并不是说你之前有没有做过的,我当时是没有反应过来,接着提示我这个可以使用循环遍历然后去删除一批的元素,我说嗯是的,其实这个问题不是在如何去批量删除元素,而是在问在HashMap中遍历删除某一元素时的注意事项?也就是HashMap在remove时需要注意的的安全方面的知识。这个其实我是有了解的,就是在Java 核心卷那本书里面有看过的,但是时间长了,有点模糊,就不能清楚的并表达出来。下来之后呢,这次我就全部代码测试了一波。

主要是把以上遍历划分为四类进行测试:迭代器方式、ForEach增强循环方式、Lambda方式和Stream API方式,测试代码如下。

1、迭代器方式

 Iterator<Map.Entry<String, Integer>> iterator2 = map.entrySet().iterator();
         while (iterator2.hasNext()){
             Map.Entry<String, Integer> entry = iterator2.next();
             if ("java".equals(entry.getKey())){
 //               map.remove(entry.getKey()); ConcurrentModificationException
                 iterator2.remove();
                 System.out.println("remove---->" + entry.getKey());
            }else {
                 System.out.println(entry.getKey() + "----" + entry.getValue());
            }
        }

 

 

这种方式是安全的,需要注意的是在删除元素时,我们需要使用iterator.remove();也就是迭代器提供的remove()方法,如果是map.remove(entry.getKey())的话,仍然是不安全的,会抛出ConcurrentModificationException异常。

2、ForEach循环方式

 for (Map.Entry<String, Integer> entry: map.entrySet()) {
     if ("java".equals(entry.getKey())){
         map.remove(entry.getKey());  // ConcurrentModificationException
         System.out.println("remove---->" + entry.getKey());
    }else {
         System.out.println(entry.getKey() + "----" + entry.getValue());
    }
 }

 

代码执行后可以看到,抛出了Exception in thread "main" java.util.ConcurrentModificationException异常,说明这种方式遍历删除元素是不安全的。

3、Lambda方式

 map.forEach((k, v)-> {
     if ("java".equals(k)){
         map.remove(k);
         System.out.println("remove---->" + k);
    }else{
         System.out.println(k + "----" + v);
    }
 });
以上代码执行后,同样抛出了Exception in thread "main" java.util.ConcurrentModificationException异常,说明这种方式遍历删除元素是不安全的。但是我们可以使用以下方式进行改进,保证安全删除。

 

 map.keySet().removeIf("java"::equals);
 map.forEach((k, v)-> {
     if ("java".equals(k)){
         map.remove(k);
         System.out.println("remove---->" + k);
    }else{
         System.out.println(k + "----" + v);
    }
 });

 

可以看到,这次在使用Lambda方式遍历删除元素之前,首先使用removeIf方法将不符合的元素进行了删除!保证安全删除。

4、Stream API方式

 map.entrySet().stream().forEach(entry->{
     if ("java".equals(entry.getKey())){
         map.remove(entry.getKey());
         System.out.println("remove---->" + entry.getKey());
    }else {
         System.out.println(entry.getKey() + "----" + entry.getValue());
    }
 });
以上代码执行后,同样抛出了Exception in thread "main" java.util.ConcurrentModificationException异常,说明这种方式遍历删除元素是不安全的。但是我们可以使用以下方式进行改进,保证安全删除。
 map.entrySet().stream().filter(entry -> !"java".equals(entry.getKey())).forEach((entry) -> {
     if ("java".equals(entry.getKey())){
         map.remove(entry.getKey());
         System.out.println("remove---->" + entry.getKey());
    }else {
         System.out.println(entry.getKey() + "----" + entry.getValue());
    }
 });

 

可以看到,这次在使用stream方式遍历删除元素之前,首先使用filter方法进行了一次过滤!保证安全删除。

小结

通过以上的测试,我们不能在遍历中使用集合 map.remove() 来删除数据,这是非安全的操作方式,但我们可以使用迭代器的 iterator.remove() 的方法来删除数据,这是安全的删除集合的方式。同样的我们也可以使用 Lambda 中的 removeIf 来提前删除数据,或者是使用 Stream 中的 filter 过滤掉要删除的数据进行循环,这样都是安全的,当然我们也可以在 for 循环前删除数据在遍历也是线程安全的。

综合性能和安全性来看,我们应该尽量使用迭代器(Iterator)来遍历 EntrySet 的遍历方式来操作 Map 集合,这样就会既安全又高效。

下面我猜会继续问的问题是:在HashMap遍历中,为什么EntrySet 比 KeySet的性能高?

然而并没有问,因为已经不需要再问HashMap这块了,已经说明基础不扎实,还需要继续学习了.....

然后就是问了几个多线程相关的问题、比如线程的创建方式、线程池的相关的问题,结果我练基本的创建线程池使用的类名都没答上来.......其实JUC编程这一块我了解的并不多,其次呢就是在准备面试时,确实没有想到被问这么小的细节,下面就是需要把并发编程这块系统学习学习,准备下常见的面试题了,因为问到JUC的几率太大了。。。。

最后就是要多注意细节,在学习记忆高频面试题时,还需要注意下小小的细节,如果这些基本的简单问题都答不上来,下面几乎可能就不再往这块追问了...背的有难度的高频题也用不到了......再有就是要适应 灵活发挥,现场随机应变,因为问到的问题不可能都是自己遇到过或者实现过的,更别说是一些场景题了。

扩展

为了解释并且验证上面的一个问题:为什么EntrySet 比 KeySet的性能高?

我在网上查询了一下,并使用性能测试工具 JMH做了验证。以下内容参考自网络。

JMH测试验证

接下来我们使用 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 轮,每次 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 = value;
        });
    }

    @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 个方法的测试结果如下:

其中 Units 为 ns/op 意思是执行完成时间(单位为纳秒),而 Score 列为平均执行时间, ± 符号表示误差。从以上结果可以看出,两个 entrySet 的性能相近,并且执行速度最快,接下来是 stream ,然后是两个 keySet,性能最差的是 KeySet

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

结论

从以上结果可以看出 entrySet 的性能比 keySet 的性能高出了一倍之多,因此我们应该尽量使用 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());
    }
}

 

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<Integer, String> entry = iterator.next()”把对象的 keyvalue 值都放入到了 Entry 对象中,因此再获取 keyvalue 值时就无需再遍历 Map 集合,只需要从 Entry 对象中取值就可以了。

所以,EntrySet 的性能比 KeySet 的性能高出了一倍,因为 KeySet 相当于循环了两遍 Map 集合,而 EntrySet 只循环了一遍

微信关注

                    编程那点事儿

 

本站为非盈利性站点,所有资源、文章等仅供学习参考,并不贩卖软件且不存在任何商业目的及用途,如果您访问和下载某文件,表示您同意只将此文件用于参考、学习而非其他用途。
本站所发布的一切软件资源、文章内容、页面内容可能整理来自于互联网,在此郑重声明本站仅限用于学习和研究目的;并告知用户不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。
如果本站相关内容有侵犯到您的合法权益,请仔细阅读本站公布的投诉指引页相关内容联系我,依法依规进行处理!
作者:理想
链接:https://www.imyjs.cn/archives/1115
THE END
二维码
面试官问我HashMap的遍历方式,我懵了….
面试开始 今天在面试过程中,面试官问HashMap的遍历……
<<上一篇
下一篇>>
文章目录
关闭
目 录