HashMap 的 7 种遍历方式与性能分析

本文最后更新于:2020年12月19日 晚上

一. HashMap 的 7 种遍历方式

  1. 迭代器(Iterator) EntrySet 遍历
  2. 迭代器(Iterator) KeySet 遍历
  3. For Each EntrySet 遍历
  4. For Each KeySet 遍历
  5. Lambda 表达式遍历
  6. stream 单线程遍历
  7. parallelStream 多线程遍历

首先初始化个集合map

static Map<Integer, String> map = new HashMap<>();
// 添加数据
static {
    map.put(1, "路飞");
    map.put(2, "索隆");
    map.put(3, "娜美");
    map.put(4, "海贼王");
    map.put(5, "尾田荣一郎");
}

1. Iterator EntrySet 遍历

@Test
public void entrySetTest() {
    Iterator<Map.Entry<Integer, String>> iterator = map.entrySet().iterator();
    while (iterator.hasNext()) {
        Map.Entry<Integer, String> entry = iterator.next();
        System.out.println(entry.getKey() + " : " + entry.getValue());
    }
}

运行输出:

1 : 路飞
2 : 索隆
3 : 娜美
4 : 海贼王
5 : 尾田荣一郎

2.Iterator KeySet 遍历

@Test
public void keySetTest() {
    Iterator<Integer> iterator = map.keySet().iterator();
    while (iterator.hasNext()) {
        Integer key = iterator.next();
        System.out.println(key + " : " + map.get(key));
    }
}

运行输出:

1 : 路飞
2 : 索隆
3 : 娜美
4 : 海贼王
5 : 尾田荣一郎

3.For Each EntrySet 遍历

@Test
public void forEachEntrySetTest() {
    for (Map.Entry<Integer, String> entry : map.entrySet()) {
        System.out.println(entry.getKey() + " : " + entry.getValue());
    }
}

运行输出:

1 : 路飞
2 : 索隆
3 : 娜美
4 : 海贼王
5 : 尾田荣一郎

4.For Each KeySet 遍历

@Test
public void forEachKeySetTest() {
    for (Integer key : map.keySet()) {
        System.out.println(key + " : " + map.get(key));
    }
}

运行输出:

1 : 路飞
2 : 索隆
3 : 娜美
4 : 海贼王
5 : 尾田荣一郎

5.Lambda 表达式 遍历

@Test
public void lambdaTest() {
    map.forEach((key, value) -> {
        System.out.println(key + " : " + value);
    });
}

运行输出:

1 : 路飞
2 : 索隆
3 : 娜美
4 : 海贼王
5 : 尾田荣一郎

6.stream 单线程遍历

@Test
public void streamTest() {
    map.entrySet().stream().forEach((entry) -> {
        System.out.println(entry.getKey() + " : " + entry.getValue());
    });
}

运行输出:

1 : 路飞
2 : 索隆
3 : 娜美
4 : 海贼王
5 : 尾田荣一郎

7.parallelStream 多线程遍历

@Test
public void parallelStreamTest() {
    // 遍历
    map.entrySet().parallelStream().forEach((entry) -> {
        System.out.println(entry.getKey() + " : " + entry.getValue());
    });
}

运行输出:

1 : 路飞
4 : 海贼王
2 : 索隆
5 : 尾田荣一郎
3 : 娜美

二.性能测试

使用 Oracle 官方提供的性能测试工具 JMH(Java Microbenchmark Harness,JAVA 微基准测试套件)来测试一下这 7 种循环的性能。

1. pom.xml 文件中添加如下配置:

<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-core</artifactId>
    <version>1.26</version>
</dependency>
<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-generator-annprocess</artifactId>
    <version>1.26</version>
    <scope>provided</scope>
</dependency>

2. 编写测试代码(不要使用debug运行)

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;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.TimeUnit;


@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(HashMapCycleTest.class.getSimpleName()) // 要导入的测试类
//                .output("D:/dev/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 个方法的测试结果如下:

Benchmark                         Mode  Cnt    Score   Error  Units
HashMapCycleTest.entrySet         avgt    5  272.079 ± 5.875  ns/op
HashMapCycleTest.forEachEntrySet  avgt    5  271.292 ± 0.625  ns/op
HashMapCycleTest.forEachKeySet    avgt    5  588.224 ± 3.972  ns/op
HashMapCycleTest.keySet           avgt    5  582.085 ± 3.305  ns/op
HashMapCycleTest.lambda           avgt    5  244.050 ± 1.182  ns/op
HashMapCycleTest.streamApi        avgt    5  430.838 ± 8.527  ns/op

其中 Units 为 ns/op 意思是执行完成时间(单位为纳秒),而 Score 列为平均执行时间, ± 符号表示误差。

3.小结

性能方面 parallelStream 多线程 > Lambda > ForEach EntrySet ~ Iterator EntrySet > Stream 单线程 > For Each KeySet ~ Iterator KeySet

三. 性能分析

将源码编译之后你会发现使用迭代器或 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 只循环了一遍。

四. 安全性测试

需求:将map里的key=3的元素删除

1. 迭代器方式

@Test
   public void iteratorDelTest() {
       Iterator<Map.Entry<Integer, String>> iterator = map.entrySet().iterator();
       while (iterator.hasNext()) {
           Map.Entry<Integer, String> entry = iterator.next();
           if (entry.getKey() == 3) {
               System.out.println("删除 " + entry.getKey() + " : " + entry.getValue());
               iterator.remove();
           }
       }
       map.forEach((key, value) -> {
           System.out.println(key + " : " + value);
       });
   }

执行结果:

删除 3 : 娜美
1 : 路飞
2 : 索隆
4 : 海贼王
5 : 尾田荣一郎

测试结果:迭代器中循环删除数据安全。

2. For 循环方式

@Test
public void forDelTest() {
    for (Map.Entry<Integer, String> entry : map.entrySet()) {
        if (entry.getKey() == 3) {
            System.out.println("删除 " + entry.getKey() + " : " + entry.getValue());
            map.remove(entry.getKey());
        }
    }
    map.forEach((key, value) -> {
        System.out.println(key + " : " + value);
    });
}

执行结果:

删除 3 : 娜美

java.util.ConcurrentModificationException
	at java.util.HashMap$HashIterator.nextNode(HashMap.java:1445)
	at java.util.HashMap$EntryIterator.next(HashMap.java:1479)
	at java.util.HashMap$EntryIterator.next(HashMap.java:1477)
	at com.liang.onepiece.demo.HashMapTest.forDelTest(HashMapTest.java:127)

测试结果:For 循环中删除数据非安全。

3. Lambda 方式

@Test
public void LambdaDelTest1() {
    map.forEach((key, value) -> {
        if (key == 3) {
            System.out.println("删除 " + key + " : " + value);
            map.remove(key);
        }
    });
    map.forEach((key, value) -> {
        System.out.println(key + " : " + value);
    });
}

执行结果:

删除 3 : 娜美

java.util.ConcurrentModificationException
	at java.util.HashMap.forEach(HashMap.java:1292)
	at com.liang.onepiece.demo.HashMapTest.LambdaDelTest1(HashMapTest.java:140)

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

Lambda 删除的正确方式(使用removeIf):

@Test
public void LambdaDelTest2() {
    // 使用 Lambda 的 removeIf 删除符合条件的数据
    map.keySet().removeIf(key -> key == 3);
    map.forEach((key, value) -> {
        System.out.println("显示 " + key + " : " + value);
    });
}

执行结果:

显示 1 : 路飞
显示 2 : 索隆
显示 4 : 海贼王
显示 5 : 尾田荣一郎

测试结果:Lambda 表达式使用 removeIf 删除数据安全。

4. Stream 方式

@Test
public void streamDelTest1() {
    map.entrySet().stream().forEach((entry) -> {
        if (entry.getKey() == 3) {
            System.out.println("删除 " + entry.getKey() + " : " + entry.getValue());
            map.remove(entry.getKey());
        }
    });
    map.forEach((key, value) -> {
        System.out.println(key + " : " + value);
    });
}

执行结果:

删除 3 : 娜美

java.util.ConcurrentModificationException
	at java.util.HashMap$EntrySpliterator.forEachRemaining(HashMap.java:1704)
	at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580)
	at com.liang.onepiece.demo.HashMapTest.streamDelTest1(HashMapTest.java:162)

测试结果:Stream 循环中删除数据非安全。

使用 Stream 中的 filter 过滤掉无用的数据,再进行遍历也是一种安全的操作集合的方式。

@Test
public void streamDelTest2() {
    map.entrySet().stream().filter(m -> 3 != m.getKey()).forEach((entry) -> {
        System.out.println("显示 " + entry.getKey() + " : " + entry.getValue());
    });
    System.out.println("---------------------");
    map.forEach((key, value) -> {
        System.out.println(key + " : " + value);
    });
}

执行结果:

显示 1 : 路飞
显示 2 : 索隆
显示 4 : 海贼王
显示 5 : 尾田荣一郎
---------------------
1 : 路飞
2 : 索隆
3 : 娜美
4 : 海贼王
5 : 尾田荣一郎

测试结果:使用 Stream 中的 filter 过滤数据数据安全,但是并不能删除map的元素。

5. 小结

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

五. 总结

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

原创为 Java中文社群


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!