面试官问我HashMap的遍历方式,我懵了….
面试开始
今天在面试过程中,面试官问HashMap的遍历方式了解吗?
此时我心想,这道题我会啊,我有记过的啊,于是就开始背书:
HashMap 遍历从大的方向来说,可分为以下 4 类:
-
迭代器(Iterator)方式遍历;
-
For Each 方式遍历;
-
Lambda 表达式遍历(JDK 1.8+);
-
Streams API 遍历(JDK 1.8+)。
但每种类型下又有不同的实现方式,因此具体的遍历方式又可以分为以下 7 种:
-
使用迭代器(Iterator)EntrySet 的方式进行遍历;
-
使用迭代器(Iterator)KeySet 的方式进行遍历;
-
使用 For Each EntrySet 的方式进行遍历;
-
使用 For Each KeySet 的方式进行遍历;
-
使用 Lambda 表达式的方式进行遍历;
-
使用 Streams API 单线程的方式进行遍历;
-
使用 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异常。 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);
}
});
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());
}
});
小结
通过以上的测试,我们不能在遍历中使用集合 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
。
结论
从以上结果可以看出 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());
}
}
而 的代码也是类似的,如下所示:
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()”把对象的 key
和 value
值都放入到了 Entry
对象中,因此再获取 key
和 value
值时就无需再遍历 Map 集合,只需要从 Entry
对象中取值就可以了。
所以,EntrySet
的性能比 KeySet
的性能高出了一倍,因为 KeySet
相当于循环了两遍 Map 集合,而 EntrySet
只循环了一遍。
微信关注