开发中我们常用到hashmap的遍历,随着jdk8的发布,又多了lambda和stream api的遍历方式,如何选择合适的遍历方式?我们可以仔细分析一下。
本文 先从 HashMap 的遍历讲起,然后对性能进行测试、分析原理、安全性,来分析各种遍历方式的优点与不足 ,本文主要内容如下图所示:
HashMap的遍历
Hashmap的遍历方式可以分为四大类
- 迭代器方式遍历
- foreach方式遍历
- Streams Api方式遍历
- lambda表达式遍历
但每种类型下又有多种的表达方式,因此具体的遍历方式又可以分为以下 7 种:
- 使用迭代器EntrySet 的方式进行遍历;
- 使用迭代器KeySet 的方式进行遍历;
- 使用 foreach EntrySet 的方式进行遍历;
- 使用 foreach KeySet 的方式进行遍历;
- 使用 Streams Api 单线程的方式进行遍历;
- 使用 Streams Api 多线程的方式(parallel)进行遍历。
- 使用 lambda表达式的方式进行遍历;
接下来我们来看每种遍历的代码具体实现
1、迭代器EntrySet
上面程序输出结果为:
2、迭代器KeySet
以上程序输出结果为:
1->java 2->hadoop 3->spring 4->elasticsearch 5->flink
3、foreach EntrySet
以上程序输出结果为:
1->java 2->hadoop 3->spring 4->elasticsearch 5->flink
4、foreach KeySet
以上程序输出结果为:
1->java 2->hadoop 3->spring 4->elasticsearch 5->flink
5、Streams Api 单线程
以上程序输出结果为:
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注解方法),测试结果如下:
其中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 最终生成的代码是一样的,如下所示:
而通过迭代器和 for 循环遍历的 KeySet 代码也是一样的,如下所示:
通过字节码可以看出,使用 EntrySet 和 KeySet 代码差别不是很大,并不像网上源码分析说的那样,KeySet 的get方法在获取Value 的时候需要进行遍历,性能上EntrySet 优于KeySet 。从测试结果看两种性能 几乎是相近的。
安全性测试
1、迭代器方式
以上程序输出结果为:
1->java删除:2
3->spring4->elasticsearch5->flink
测试总结:迭代器方式删除数据安全
2、for循环方式
以上程序输出结果为:
测试总结:for循环遍历方式删除数据不安全
3、lambda方式
以上程序输出结果为:
测试总结:lambda方式遍历删除数据不安全
4、stream 方式
以上程序运行结果为:
测试总结:streams方式遍历删除数据不安全
小结
我们不能在for循环遍历中使用时来删除map里面的数据,这是非安全的操作方式,但我们可以使用迭代器的 iterator.remove() 的方法来删除数据,这是安全的操作方式。同样的lambada和stream在使用的时候删除集合里面的数据是非安全的操作方式。但是我们可以在使用之前删除集合数据然后进行遍历。
总结
本文主要讲了hashmap的四大类遍历方式(迭代器,for循环、lambda、streams),以及具体的7种遍历方式代码, 除了stream的并行遍历外,其他几种遍历方式在性能上没有明显的差别,但是从代码的简洁和优雅上来看,lambda和stream有先天的优势。 除此之外,还从安全性的方面测试了四类遍历在运行时删除数据的安全性。 除了迭代方式外,其他三类在循环遍历的时候,删除数据是非安全的。但是我们可以先在遍历之前删除数据,然后进行遍历,这个操作方式是安全的。
总体来说,本文提供了 7 种常见的遍历方式,肯定不是最全的,我是想给读者在使用 HashMap 时选择最合适的方式, 然而选择哪一种写法,在性能、JDK 版本(是否1.8)、安全性以及优雅和可读性等方面来综合考虑 。最后,欢迎各位在评论区补充并留言,写出你们的想法。